Skip to content

Commit

Permalink
native: extend CryptoLib's verifyWithECDsa with hasher parameter
Browse files Browse the repository at this point in the history
Replace native CryptoLib's verifyWithECDsa `curve` parameter by
`curveHash` parameter which is a enum over supported pairs of named
curves and hash functions.

Even though this change is a compatible extension of the protocol, it
changes the genesis state due to parameter renaming. But we're going to
resync chain in 3.7 release anyway, so it's not a big deal.

Also, we need to check mainnet and testnet compatibility in case if
anyone has ever called verifyWithECDsa with 24 or 25 `curve` value.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
  • Loading branch information
AnnaShaleva committed May 3, 2024
1 parent 17a99aa commit 1e2b438
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 74 deletions.
8 changes: 5 additions & 3 deletions pkg/compiler/native_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ func TestRoleManagementRole(t *testing.T) {
}

func TestCryptoLibNamedCurve(t *testing.T) {
require.EqualValues(t, native.Secp256k1, crypto.Secp256k1)
require.EqualValues(t, native.Secp256r1, crypto.Secp256r1)
require.EqualValues(t, native.Secp256k1Sha256, crypto.Secp256k1Sha256)
require.EqualValues(t, native.Secp256r1Sha256, crypto.Secp256r1Sha256)
require.EqualValues(t, native.Secp256k1Keccak256, crypto.Secp256k1Keccak256)
require.EqualValues(t, native.Secp256r1Keccak256, crypto.Secp256r1Keccak256)
}

func TestOracleContractValues(t *testing.T) {
Expand Down Expand Up @@ -233,7 +235,7 @@ func TestNativeHelpersCompile(t *testing.T) {
{"sha256", []string{"[]byte{1, 2, 3}"}},
{"ripemd160", []string{"[]byte{1, 2, 3}"}},
{"murmur32", []string{"[]byte{1, 2, 3}", "123"}},
{"verifyWithECDsa", []string{"[]byte{1, 2, 3}", pub, sig, "crypto.Secp256k1"}},
{"verifyWithECDsa", []string{"[]byte{1, 2, 3}", pub, sig, "crypto.Secp256k1Sha256"}},
{"bls12381Serialize", []string{"crypto.Bls12381Point{}"}},
{"bls12381Deserialize", []string{"[]byte{1, 2, 3}"}},
{"bls12381Equal", []string{"crypto.Bls12381Point{}", "crypto.Bls12381Point{}"}},
Expand Down
53 changes: 28 additions & 25 deletions pkg/core/native/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,28 @@ import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/twmb/murmur3"
"golang.org/x/crypto/sha3"
)

// Crypto represents CryptoLib contract.
type Crypto struct {
interop.ContractMD
}

// NamedCurve identifies named elliptic curves.
type NamedCurve byte
// HashFunc is a delegate representing a hasher function with 256 bytes output length.
type HashFunc func([]byte) util.Uint256

// Various named elliptic curves.
// NamedCurveHash identifies a pair of named elliptic curve and hash function.
type NamedCurveHash byte

// Various pairs of named elliptic curves and hash functions.
const (
Secp256k1 NamedCurve = 22
Secp256r1 NamedCurve = 23
Secp256k1Sha256 NamedCurveHash = 22
Secp256r1Sha256 NamedCurveHash = 23
Secp256k1Keccak256 NamedCurveHash = 24
Secp256r1Keccak256 NamedCurveHash = 25
)

const cryptoContractID = -3
Expand Down Expand Up @@ -63,7 +68,7 @@ func newCrypto() *Crypto {
manifest.NewParameter("message", smartcontract.ByteArrayType),
manifest.NewParameter("pubkey", smartcontract.ByteArrayType),
manifest.NewParameter("signature", smartcontract.ByteArrayType),
manifest.NewParameter("curve", smartcontract.IntegerType))
manifest.NewParameter("curveHash", smartcontract.IntegerType))
md = newMethodAndPrice(c.verifyWithECDsa, 1<<15, callflag.NoneFlag)
c.AddMethod(md, desc)

Expand Down Expand Up @@ -142,7 +147,6 @@ func (c *Crypto) verifyWithECDsa(_ *interop.Context, args []stackitem.Item) stac
if err != nil {
panic(fmt.Errorf("invalid message stackitem: %w", err))
}
hashToCheck := hash.Sha256(msg)
pubkey, err := args[1].TryBytes()
if err != nil {
panic(fmt.Errorf("invalid pubkey stackitem: %w", err))
Expand All @@ -151,10 +155,11 @@ func (c *Crypto) verifyWithECDsa(_ *interop.Context, args []stackitem.Item) stac
if err != nil {
panic(fmt.Errorf("invalid signature stackitem: %w", err))
}
curve, err := curveFromStackitem(args[3])
curve, hasher, err := curveHasherFromStackitem(args[3])
if err != nil {
panic(fmt.Errorf("invalid curve stackitem: %w", err))
panic(fmt.Errorf("invalid curveHash stackitem: %w", err))
}
hashToCheck := hasher(msg)
pkey, err := keys.NewPublicKeyFromBytes(pubkey, curve)
if err != nil {
panic(fmt.Errorf("failed to decode pubkey: %w", err))
Expand All @@ -163,22 +168,26 @@ func (c *Crypto) verifyWithECDsa(_ *interop.Context, args []stackitem.Item) stac
return stackitem.NewBool(res)
}

func curveFromStackitem(si stackitem.Item) (elliptic.Curve, error) {
func curveHasherFromStackitem(si stackitem.Item) (elliptic.Curve, HashFunc, error) {
curve, err := si.TryInteger()
if err != nil {
return nil, err
return nil, nil, err
}
if !curve.IsInt64() {
return nil, errors.New("not an int64")
return nil, nil, errors.New("not an int64")
}
c := curve.Int64()
switch c {
case int64(Secp256k1):
return secp256k1.S256(), nil
case int64(Secp256r1):
return elliptic.P256(), nil
case int64(Secp256k1Sha256):
return secp256k1.S256(), hash.Sha256, nil
case int64(Secp256r1Sha256):
return elliptic.P256(), hash.Sha256, nil
case int64(Secp256k1Keccak256):
return secp256k1.S256(), hash.Keccak256, nil
case int64(Secp256r1Keccak256):
return elliptic.P256(), hash.Keccak256, nil
default:
return nil, errors.New("unsupported curve type")
return nil, nil, errors.New("unsupported curve/hash type")
}
}

Expand Down Expand Up @@ -295,13 +304,7 @@ func (c *Crypto) keccak256(_ *interop.Context, args []stackitem.Item) stackitem.
if err != nil {
panic(err)
}

digest := sha3.NewLegacyKeccak256()
_, err = digest.Write(bs)
if err != nil {
panic(err)
}
return stackitem.NewByteArray(digest.Sum(nil))
return stackitem.NewByteArray(hash.Keccak256(bs).BytesBE())
}

// Metadata implements the Contract interface.
Expand Down
34 changes: 25 additions & 9 deletions pkg/core/native/crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/consensys/gnark-crypto/ecc/bls12-381/fr"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
Expand Down Expand Up @@ -118,29 +119,44 @@ func TestMurmur32(t *testing.T) {
}

func TestCryptoLibVerifyWithECDsa(t *testing.T) {
t.Run("R1", func(t *testing.T) {
testECDSAVerify(t, Secp256r1)
t.Run("R1 sha256", func(t *testing.T) {
testECDSAVerify(t, Secp256r1Sha256)
})
t.Run("K1", func(t *testing.T) {
testECDSAVerify(t, Secp256k1)
t.Run("K1 sha256", func(t *testing.T) {
testECDSAVerify(t, Secp256k1Sha256)
})
t.Run("R1 keccak256", func(t *testing.T) {
testECDSAVerify(t, Secp256r1Keccak256)
})
t.Run("K1 keccak256", func(t *testing.T) {
testECDSAVerify(t, Secp256k1Keccak256)
})
}

func testECDSAVerify(t *testing.T, curve NamedCurve) {
func testECDSAVerify(t *testing.T, curve NamedCurveHash) {
var (
priv *keys.PrivateKey
err error
c = newCrypto()
ic = &interop.Context{VM: vm.New()}
actual stackitem.Item
hasher HashFunc
)
switch curve {
case Secp256k1:
case Secp256k1Sha256:
priv, err = keys.NewSecp256k1PrivateKey()
hasher = hash.Sha256
case Secp256r1Sha256:
priv, err = keys.NewPrivateKey()
hasher = hash.Sha256
case Secp256k1Keccak256:
priv, err = keys.NewSecp256k1PrivateKey()
case Secp256r1:
hasher = hash.Keccak256
case Secp256r1Keccak256:
priv, err = keys.NewPrivateKey()
hasher = hash.Keccak256
default:
t.Fatal("unknown curve")
t.Fatal("unknown curve/hash")
}
require.NoError(t, err)

Expand All @@ -162,7 +178,7 @@ func testECDSAVerify(t *testing.T, curve NamedCurve) {
}

msg := []byte("test message")
sign := priv.Sign(msg)
sign := priv.SignHash(hasher(msg))

t.Run("bad message item", func(t *testing.T) {
runCase(t, true, false, stackitem.NewInterop("cheburek"), priv.PublicKey().Bytes(), sign, int64(curve))
Expand Down
57 changes: 28 additions & 29 deletions pkg/core/native/native_test/cryptolib_verification_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,13 @@ func TestCryptoLib_KoblitzVerificationScript(t *testing.T) {
// This transaction (along with the network magic) should be signed by the user's Koblitz private key.
msg := constructMsg(t, uint32(e.Chain.GetConfig().Magic), tx)

// The user has to sign the Sha256 hash of the message by his Koblitz key.
// Please, note that this Sha256 hash may easily be replaced by Keccaak hash via minor adjustment of
// CryptoLib's `verifyWithECDsa` behaviour (if needed).
signature := pk.SignHash(hash.Sha256(msg))
// The user has to sign the hash of the message by his Koblitz key.
// Please, note that this Keccak256 hash may easily be replaced by sha256 hash if needed.
signature := pk.SignHash(hash.Keccak256(msg))

// Ensure that signature verification passes. This line here is just for testing purposes,
// it won't be present in the real code.
require.True(t, pk.PublicKey().Verify(signature, hash.Sha256(msg).BytesBE()))
require.True(t, pk.PublicKey().Verify(signature, hash.Keccak256(msg).BytesBE()))

// Build invocation witness script for the user's account.
invBytes := buildKoblitzInvocationScript(t, signature)
Expand All @@ -105,32 +104,32 @@ func TestCryptoLib_KoblitzVerificationScript(t *testing.T) {

// The simplest witness verification script with low length and low execution cost
// (98 bytes, 2092530 GAS including Invocation script execution).
// The user has to sign the sha256([var-bytes-network-magic, txHash-bytes-BE]).
// The user has to sign the keccak256([var-bytes-network-magic, txHash-bytes-BE]).
check(t, buildKoblitzVerificationScriptSimpleSingleHash, constructMessageNoHash)

// Even more simple witness verification script with low length and low execution cost
// (95 bytes, 2092320 GAS including Invocation script execution).
// The user has to sign the sha256([var-bytes-network-magic, txHash-bytes-BE]).
// The user has to sign the keccak256([var-bytes-network-magic, txHash-bytes-BE]).
// The difference is that network magic is a static value, thus, both verification script and
// user address are network-specific.
check(t, buildKoblitzVerificationScriptSimpleSingleHashStaticMagic, constructMessageNoHash)

// More complicated verification script with higher length and higher execution cost
// (136 bytes, 4120620 GAS including Invocation script execution).
// The user has to sign the sha256(sha256([var-bytes-network-magic, txHash-bytes-BE])).
// The user has to sign the keccak256(sha256([var-bytes-network-magic, txHash-bytes-BE])).
check(t, buildKoblitzVerificationScriptSimple, constructMessageSimple)

// Witness verification script that follows the existing standard CheckSig account generation rules
// and has larger length and higher execution cost.
// (186 bytes, 5116020 GAS including Invocation script execution).
// The user has to sign the sha256(sha256([4-bytes-network-magic-LE, txHash-bytes-BE]))
// The user has to sign the keccak256(sha256([4-bytes-network-magic-LE, txHash-bytes-BE]))
check(t, buildKoblitzVerificationScriptCompat, constructMessageCompat)
}

// buildKoblitzVerificationScriptSimpleSingleHash builds witness verification script for Koblitz public key.
// This method differs from buildKoblitzVerificationScriptCompat in that it checks
//
// sha256([var-bytes-network-magic, txHash-bytes-BE])
// keccak256([var-bytes-network-magic, txHash-bytes-BE])
//
// instead of (comparing with N3)
//
Expand All @@ -141,9 +140,9 @@ func buildKoblitzVerificationScriptSimpleSingleHash(t *testing.T, pub *keys.Publ
// vrf is witness verification script corresponding to the pub.
// vrf is witness verification script corresponding to the pk.
vrf := io.NewBufBinWriter()
emit.Int(vrf.BinWriter, int64(native.Secp256k1)) // push Koblitz curve identifier.
emit.Opcodes(vrf.BinWriter, opcode.SWAP) // swap curve identifier with the signature.
emit.Bytes(vrf.BinWriter, pub.Bytes()) // emit the caller's public key.
emit.Int(vrf.BinWriter, int64(native.Secp256k1Keccak256)) // push Koblitz curve identifier.
emit.Opcodes(vrf.BinWriter, opcode.SWAP) // swap curve identifier with the signature.
emit.Bytes(vrf.BinWriter, pub.Bytes()) // emit the caller's public key.
// Construct and push the signed message. The signed message is effectively the network-dependent transaction hash,
// i.e. msg = [network-magic-bytes, tx.Hash()]
// Firstly, retrieve network magic (it's uint32 wrapped into BigInteger and represented as Integer stackitem on stack).
Expand All @@ -164,7 +163,7 @@ func buildKoblitzVerificationScriptSimpleSingleHash(t *testing.T, pub *keys.Publ
// READY: loaded 98 instructions
// NEO-GO-VM 0 > ops
// INDEX OPCODE PARAMETER
// 0 PUSHINT8 22 (16) <<
// 0 PUSHINT8 24 (18) <<
// 2 SWAP
// 3 PUSHDATA1 0363d7a48125a76cdea6e098c9f128e82920ed428e5fb4caf1d7f81c16cad0c205
// 38 SYSCALL System.Runtime.GetNetwork (c5fba0e0)
Expand All @@ -183,7 +182,7 @@ func buildKoblitzVerificationScriptSimpleSingleHash(t *testing.T, pub *keys.Publ
// buildKoblitzVerificationScriptSimpleSingleHashStaticMagic builds witness verification script for Koblitz public key.
// This method differs from buildKoblitzVerificationScriptCompat in that it checks
//
// sha256([var-bytes-network-magic, txHash-bytes-BE])
// keccak256([var-bytes-network-magic, txHash-bytes-BE])
//
// instead of (comparing with N3)
//
Expand All @@ -197,9 +196,9 @@ func buildKoblitzVerificationScriptSimpleSingleHashStaticMagic(t *testing.T, pub
// vrf is witness verification script corresponding to the pub.
// vrf is witness verification script corresponding to the pk.
vrf := io.NewBufBinWriter()
emit.Int(vrf.BinWriter, int64(native.Secp256k1)) // push Koblitz curve identifier.
emit.Opcodes(vrf.BinWriter, opcode.SWAP) // swap curve identifier with the signature.
emit.Bytes(vrf.BinWriter, pub.Bytes()) // emit the caller's public key.
emit.Int(vrf.BinWriter, int64(native.Secp256k1Keccak256)) // push Koblitz curve identifier.
emit.Opcodes(vrf.BinWriter, opcode.SWAP) // swap curve identifier with the signature.
emit.Bytes(vrf.BinWriter, pub.Bytes()) // emit the caller's public key.
// Construct and push the signed message. The signed message is effectively the network-dependent transaction hash,
// i.e. msg = [network-magic-bytes, tx.Hash()]
// Firstly, push static network magic (it's 42 for unit test chain).
Expand All @@ -220,7 +219,7 @@ func buildKoblitzVerificationScriptSimpleSingleHashStaticMagic(t *testing.T, pub
// READY: loaded 95 instructions
// NEO-GO-VM 0 > ops
// INDEX OPCODE PARAMETER
// 0 PUSHINT8 22 (16) <<
// 0 PUSHINT8 24 (18) <<
// 2 SWAP
// 3 PUSHDATA1 0296e13080ade92a2ab722338c2a249ee8d83a14f649c68321664165f06bd110bd
// 38 PUSHINT8 42 (2a)
Expand All @@ -239,7 +238,7 @@ func buildKoblitzVerificationScriptSimpleSingleHashStaticMagic(t *testing.T, pub
// buildKoblitzVerificationScriptSimple builds witness verification script for Koblitz public key.
// This method differs from buildKoblitzVerificationScriptCompat in that it checks
//
// sha256(sha256([var-bytes-network-magic, txHash-bytes-BE]))
// keccak256(sha256([var-bytes-network-magic, txHash-bytes-BE]))
//
// instead of (comparing with N3)
//
Expand All @@ -255,9 +254,9 @@ func buildKoblitzVerificationScriptSimple(t *testing.T, pub *keys.PublicKey) []b
// vrf is witness verification script corresponding to the pub.
// vrf is witness verification script corresponding to the pk.
vrf := io.NewBufBinWriter()
emit.Int(vrf.BinWriter, int64(native.Secp256k1)) // push Koblitz curve identifier.
emit.Opcodes(vrf.BinWriter, opcode.SWAP) // swap curve identifier with the signature.
emit.Bytes(vrf.BinWriter, pub.Bytes()) // emit the caller's public key.
emit.Int(vrf.BinWriter, int64(native.Secp256k1Keccak256)) // push Koblitz curve identifier.
emit.Opcodes(vrf.BinWriter, opcode.SWAP) // swap curve identifier with the signature.
emit.Bytes(vrf.BinWriter, pub.Bytes()) // emit the caller's public key.
// Construct and push the signed message. The signed message is effectively the network-dependent transaction hash,
// i.e. msg = Sha256([network-magic-bytes, tx.Hash()])
// Firstly, retrieve network magic (it's uint32 wrapped into BigInteger and represented as Integer stackitem on stack).
Expand All @@ -280,7 +279,7 @@ func buildKoblitzVerificationScriptSimple(t *testing.T, pub *keys.PublicKey) []b
// READY: loaded 136 instructions
// NEO-GO-VM 0 > ops
// INDEX OPCODE PARAMETER
// 0 PUSHINT8 22 (16) <<
// 0 PUSHINT8 24 (18) <<
// 2 SWAP
// 3 PUSHDATA1 03a77f137afbb4b68d7a450aa5a28fe335f804c589a808494b4b626eb98707f37d
// 38 SYSCALL System.Runtime.GetNetwork (c5fba0e0)
Expand All @@ -305,17 +304,17 @@ func buildKoblitzVerificationScriptSimple(t *testing.T, pub *keys.PublicKey) []b
// buildKoblitzVerificationScript builds custom verification script for the provided Koblitz public key.
// It checks that the following message is signed by the provided public key:
//
// sha256(sha256([4-bytes-network-magic-LE, txHash-bytes-BE]))
// keccak256(sha256([4-bytes-network-magic-LE, txHash-bytes-BE]))
//
// It produces constant-length verification script (186 bytes) independently of the network parameters.
func buildKoblitzVerificationScriptCompat(t *testing.T, pub *keys.PublicKey) []byte {
criptoLibH := state.CreateNativeContractHash(nativenames.CryptoLib)

// vrf is witness verification script corresponding to the pub.
vrf := io.NewBufBinWriter()
emit.Int(vrf.BinWriter, int64(native.Secp256k1)) // push Koblitz curve identifier.
emit.Opcodes(vrf.BinWriter, opcode.SWAP) // swap curve identifier with the signature.
emit.Bytes(vrf.BinWriter, pub.Bytes()) // emit the caller's public key.
emit.Int(vrf.BinWriter, int64(native.Secp256k1Keccak256)) // push Koblitz curve identifier.
emit.Opcodes(vrf.BinWriter, opcode.SWAP) // swap curve identifier with the signature.
emit.Bytes(vrf.BinWriter, pub.Bytes()) // emit the caller's public key.
// Construct and push the signed message. The signed message is effectively the network-dependent transaction hash,
// i.e. msg = Sha256([4-bytes-network-magic-LE, tx.Hash()])
// Firstly, convert network magic (uint32) to LE buffer.
Expand Down Expand Up @@ -381,7 +380,7 @@ func buildKoblitzVerificationScriptCompat(t *testing.T, pub *keys.PublicKey) []b
// READY: loaded 186 instructions
// NEO-GO-VM 0 > ops
// INDEX OPCODE PARAMETER
// 0 PUSHINT8 22 (16) <<
// 0 PUSHINT8 24 (18) <<
// 2 SWAP
// 3 PUSHDATA1 02627ef9c3631e3ccb8fbc4c5b6c49e38ccede5a79afb1e1b0708fbb958a7802d7
// 38 SYSCALL System.Runtime.GetNetwork (c5fba0e0)
Expand Down
Loading

0 comments on commit 1e2b438

Please sign in to comment.