From 455664f7845fb67307f5ed195466a9ca51f6ac5d Mon Sep 17 00:00:00 2001 From: beer-1 <147697694+beer-1@users.noreply.github.com> Date: Fri, 1 Dec 2023 20:16:15 +0900 Subject: [PATCH] add eth secp256k1 support --- app/encoding.go | 3 + crypto/codec/amino.go | 13 + crypto/codec/proto.go | 13 + crypto/keys/eth/secp256k1/pubkey.pb.go | 329 ++++++++++++++++++ crypto/keys/eth/secp256k1/secp256k1.go | 93 +++++ crypto/keys/eth/secp256k1/secp256k1_cgo.go | 16 + .../keys/eth/secp256k1/secp256k1_cgo_test.go | 41 +++ crypto/keys/eth/secp256k1/secp256k1_nocgo.go | 47 +++ .../eth/secp256k1/secp256k1_nocgo_test.go | 50 +++ crypto/keys/eth/secp256k1/secp256k1_test.go | 203 +++++++++++ .../initia/crypto/eth/secp256k1/pubkey.proto | 30 ++ 11 files changed, 838 insertions(+) create mode 100644 crypto/codec/amino.go create mode 100644 crypto/codec/proto.go create mode 100644 crypto/keys/eth/secp256k1/pubkey.pb.go create mode 100644 crypto/keys/eth/secp256k1/secp256k1.go create mode 100644 crypto/keys/eth/secp256k1/secp256k1_cgo.go create mode 100644 crypto/keys/eth/secp256k1/secp256k1_cgo_test.go create mode 100644 crypto/keys/eth/secp256k1/secp256k1_nocgo.go create mode 100644 crypto/keys/eth/secp256k1/secp256k1_nocgo_test.go create mode 100644 crypto/keys/eth/secp256k1/secp256k1_test.go create mode 100644 proto/initia/crypto/eth/secp256k1/pubkey.proto diff --git a/app/encoding.go b/app/encoding.go index 6d1dd73d..3bf28b87 100644 --- a/app/encoding.go +++ b/app/encoding.go @@ -5,6 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/std" "github.com/initia-labs/initia/app/params" + cryptocodec "github.com/initia-labs/initia/crypto/codec" ) var legacyCodecRegistered = false @@ -14,6 +15,8 @@ func MakeEncodingConfig() params.EncodingConfig { encodingConfig := params.MakeEncodingConfig() std.RegisterLegacyAminoCodec(encodingConfig.Amino) std.RegisterInterfaces(encodingConfig.InterfaceRegistry) + cryptocodec.RegisterCrypto(encodingConfig.Amino) + cryptocodec.RegisterInterfaces(encodingConfig.InterfaceRegistry) ModuleBasics.RegisterLegacyAminoCodec(encodingConfig.Amino) ModuleBasics.RegisterInterfaces(encodingConfig.InterfaceRegistry) diff --git a/crypto/codec/amino.go b/crypto/codec/amino.go new file mode 100644 index 00000000..6154f55e --- /dev/null +++ b/crypto/codec/amino.go @@ -0,0 +1,13 @@ +package codec + +import ( + "github.com/cosmos/cosmos-sdk/codec" + + ethsecp256k1 "github.com/initia-labs/initia/crypto/keys/eth/secp256k1" +) + +// RegisterCrypto registers all crypto dependency types with the provided Amino +// codec. +func RegisterCrypto(cdc *codec.LegacyAmino) { + cdc.RegisterConcrete(ðsecp256k1.PubKey{}, ethsecp256k1.PubKeyName, nil) +} diff --git a/crypto/codec/proto.go b/crypto/codec/proto.go new file mode 100644 index 00000000..0c124799 --- /dev/null +++ b/crypto/codec/proto.go @@ -0,0 +1,13 @@ +package codec + +import ( + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + + ethsecp256k1 "github.com/initia-labs/initia/crypto/keys/eth/secp256k1" +) + +// RegisterInterfaces registers the sdk.Tx interface. +func RegisterInterfaces(registry codectypes.InterfaceRegistry) { + registry.RegisterImplementations((*cryptotypes.PubKey)(nil), ðsecp256k1.PubKey{}) +} diff --git a/crypto/keys/eth/secp256k1/pubkey.pb.go b/crypto/keys/eth/secp256k1/pubkey.pb.go new file mode 100644 index 00000000..4158b564 --- /dev/null +++ b/crypto/keys/eth/secp256k1/pubkey.pb.go @@ -0,0 +1,329 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: initia/crypto/eth/secp256k1/pubkey.proto + +package secp256k1 + +import ( + fmt "fmt" + _ "github.com/cosmos/cosmos-sdk/types/tx/amino" + _ "github.com/cosmos/gogoproto/gogoproto" + proto "github.com/cosmos/gogoproto/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// PubKey defines a eth_secp256k1 public key +// Key is the compressed form of the pubkey. The first byte depends is a 0x02 byte +// if the y-coordinate is the lexicographically largest of the two associated with +// the x-coordinate. Otherwise the first byte is a 0x03. +// This prefix is followed with the x-coordinate. +type PubKey struct { + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` +} + +func (m *PubKey) Reset() { *m = PubKey{} } +func (*PubKey) ProtoMessage() {} +func (*PubKey) Descriptor() ([]byte, []int) { + return fileDescriptor_8fdf6a8c6891b59f, []int{0} +} +func (m *PubKey) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *PubKey) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_PubKey.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *PubKey) XXX_Merge(src proto.Message) { + xxx_messageInfo_PubKey.Merge(m, src) +} +func (m *PubKey) XXX_Size() int { + return m.Size() +} +func (m *PubKey) XXX_DiscardUnknown() { + xxx_messageInfo_PubKey.DiscardUnknown(m) +} + +var xxx_messageInfo_PubKey proto.InternalMessageInfo + +func (m *PubKey) GetKey() []byte { + if m != nil { + return m.Key + } + return nil +} + +func init() { + proto.RegisterType((*PubKey)(nil), "initia.crypto.eth.secp256k1.PubKey") +} + +func init() { + proto.RegisterFile("initia/crypto/eth/secp256k1/pubkey.proto", fileDescriptor_8fdf6a8c6891b59f) +} + +var fileDescriptor_8fdf6a8c6891b59f = []byte{ + // 229 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xd2, 0xc8, 0xcc, 0xcb, 0x2c, + 0xc9, 0x4c, 0xd4, 0x4f, 0x2e, 0xaa, 0x2c, 0x28, 0xc9, 0xd7, 0x4f, 0x2d, 0xc9, 0xd0, 0x2f, 0x4e, + 0x4d, 0x2e, 0x30, 0x32, 0x35, 0xcb, 0x36, 0xd4, 0x2f, 0x28, 0x4d, 0xca, 0x4e, 0xad, 0xd4, 0x2b, + 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x92, 0x86, 0xa8, 0xd4, 0x83, 0xa8, 0xd4, 0x4b, 0x2d, 0xc9, 0xd0, + 0x83, 0xab, 0x94, 0x12, 0x4c, 0xcc, 0xcd, 0xcc, 0xcb, 0xd7, 0x07, 0x93, 0x10, 0xf5, 0x52, 0x22, + 0xe9, 0xf9, 0xe9, 0xf9, 0x60, 0xa6, 0x3e, 0x88, 0x05, 0x11, 0x55, 0xf2, 0xe1, 0x62, 0x0b, 0x28, + 0x4d, 0xf2, 0x4e, 0xad, 0x14, 0x12, 0xe0, 0x62, 0xce, 0x4e, 0xad, 0x94, 0x60, 0x54, 0x60, 0xd4, + 0xe0, 0x09, 0x02, 0x31, 0xad, 0x0c, 0x66, 0x2c, 0x90, 0x67, 0xe8, 0x7a, 0xbe, 0x41, 0x4b, 0x12, + 0xea, 0x28, 0x88, 0x4a, 0xd7, 0x92, 0x8c, 0x60, 0x98, 0x45, 0x93, 0x9e, 0x6f, 0xd0, 0xe2, 0xcc, + 0x4e, 0xad, 0x8c, 0x4f, 0xcb, 0x4c, 0xcd, 0x49, 0x71, 0x0a, 0x3c, 0xf1, 0x48, 0x8e, 0xf1, 0xc2, + 0x23, 0x39, 0xc6, 0x07, 0x8f, 0xe4, 0x18, 0x27, 0x3c, 0x96, 0x63, 0xb8, 0xf0, 0x58, 0x8e, 0xe1, + 0xc6, 0x63, 0x39, 0x86, 0x28, 0xf3, 0xf4, 0xcc, 0x92, 0x8c, 0xd2, 0x24, 0xbd, 0xe4, 0xfc, 0x5c, + 0x7d, 0x88, 0x69, 0xba, 0x39, 0x89, 0x49, 0xc5, 0xfa, 0xa8, 0xde, 0xcd, 0x4e, 0xad, 0x2c, 0x46, + 0xf5, 0x73, 0x12, 0x1b, 0xd8, 0x9d, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xd8, 0x8c, 0xe5, + 0x5d, 0x19, 0x01, 0x00, 0x00, +} + +func (m *PubKey) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *PubKey) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *PubKey) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Key) > 0 { + i -= len(m.Key) + copy(dAtA[i:], m.Key) + i = encodeVarintPubkey(dAtA, i, uint64(len(m.Key))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintPubkey(dAtA []byte, offset int, v uint64) int { + offset -= sovPubkey(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *PubKey) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Key) + if l > 0 { + n += 1 + l + sovPubkey(uint64(l)) + } + return n +} + +func sovPubkey(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozPubkey(x uint64) (n int) { + return sovPubkey(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *PubKey) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPubkey + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: PubKey: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: PubKey: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPubkey + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthPubkey + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthPubkey + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Key = append(m.Key[:0], dAtA[iNdEx:postIndex]...) + if m.Key == nil { + m.Key = []byte{} + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipPubkey(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthPubkey + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipPubkey(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowPubkey + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowPubkey + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowPubkey + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthPubkey + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupPubkey + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthPubkey + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthPubkey = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowPubkey = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupPubkey = fmt.Errorf("proto: unexpected end of group") +) diff --git a/crypto/keys/eth/secp256k1/secp256k1.go b/crypto/keys/eth/secp256k1/secp256k1.go new file mode 100644 index 00000000..b626b8d5 --- /dev/null +++ b/crypto/keys/eth/secp256k1/secp256k1.go @@ -0,0 +1,93 @@ +package secp256k1 + +import ( + "bytes" + "fmt" + + secp256k1 "github.com/btcsuite/btcd/btcec/v2" + "github.com/cometbft/cometbft/crypto" //nolint: staticcheck + "golang.org/x/crypto/sha3" + + "github.com/cosmos/cosmos-sdk/codec" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/types/errors" +) + +const ( + keyType = "eth_secp256k1" + PubKeyName = "initia/PubKeyEthSecp256k1" +) + +//------------------------------------- + +var ( + _ cryptotypes.PubKey = &PubKey{} + _ codec.AminoMarshaler = &PubKey{} +) + +// PubKeySize is comprised of 32 bytes for one field element +// (the x-coordinate), plus one byte for the parity of the y-coordinate. +const PubKeySize = 33 + +// Address returns a Bitcoin style addresses: RIPEMD160(SHA256(pubkey)) +func (pubKey *PubKey) Address() crypto.Address { + if len(pubKey.Key) != PubKeySize { + panic("length of pubkey is incorrect") + } + + pk, err := secp256k1.ParsePubKey(pubKey.Key) + if err != nil { + panic(err) + } + + uncompressedBytes := pk.SerializeUncompressed() + hasher := sha3.NewLegacyKeccak256() + hasher.Write(uncompressedBytes[1:]) + bz := hasher.Sum(nil) + + return bz[len(bz)-20:] +} + +// Bytes returns the pubkey byte format. +func (pubKey *PubKey) Bytes() []byte { + return pubKey.Key +} + +func (pubKey *PubKey) String() string { + return fmt.Sprintf("PubKeyEthSecp256k1{%X}", pubKey.Key) +} + +func (pubKey *PubKey) Type() string { + return keyType +} + +func (pubKey *PubKey) Equals(other cryptotypes.PubKey) bool { + return pubKey.Type() == other.Type() && bytes.Equal(pubKey.Bytes(), other.Bytes()) +} + +// MarshalAmino overrides Amino binary marshalling. +func (pubKey PubKey) MarshalAmino() ([]byte, error) { + return pubKey.Key, nil +} + +// UnmarshalAmino overrides Amino binary marshalling. +func (pubKey *PubKey) UnmarshalAmino(bz []byte) error { + if len(bz) != PubKeySize { + return errors.Wrap(errors.ErrInvalidPubKey, "invalid pubkey size") + } + pubKey.Key = bz + + return nil +} + +// MarshalAminoJSON overrides Amino JSON marshalling. +func (pubKey PubKey) MarshalAminoJSON() ([]byte, error) { + // When we marshal to Amino JSON, we don't marshal the "key" field itself, + // just its contents (i.e. the key bytes). + return pubKey.MarshalAmino() +} + +// UnmarshalAminoJSON overrides Amino JSON marshalling. +func (pubKey *PubKey) UnmarshalAminoJSON(bz []byte) error { + return pubKey.UnmarshalAmino(bz) +} diff --git a/crypto/keys/eth/secp256k1/secp256k1_cgo.go b/crypto/keys/eth/secp256k1/secp256k1_cgo.go new file mode 100644 index 00000000..b58a4981 --- /dev/null +++ b/crypto/keys/eth/secp256k1/secp256k1_cgo.go @@ -0,0 +1,16 @@ +//go:build libsecp256k1_sdk +// +build libsecp256k1_sdk + +package secp256k1 + +import ( + "github.com/cometbft/cometbft/crypto" + + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1/internal/secp256k1" +) + +// VerifySignature validates the signature. +// The msg will be hashed prior to signature verification. +func (pubKey *PubKey) VerifySignature(msg []byte, sigStr []byte) bool { + return secp256k1.VerifySignature(pubKey.Bytes(), crypto.Sha256(msg), sigStr) +} diff --git a/crypto/keys/eth/secp256k1/secp256k1_cgo_test.go b/crypto/keys/eth/secp256k1/secp256k1_cgo_test.go new file mode 100644 index 00000000..00128e92 --- /dev/null +++ b/crypto/keys/eth/secp256k1/secp256k1_cgo_test.go @@ -0,0 +1,41 @@ +//go:build libsecp256k1_sdk +// +build libsecp256k1_sdk + +package secp256k1 + +import ( + "testing" + + "github.com/magiconair/properties/assert" + + "github.com/stretchr/testify/require" +) + +func TestPrivKeySecp256k1SignVerify(t *testing.T) { + msg := []byte("A.1.2 ECC Key Pair Generation by Testing Candidates") + priv := GenPrivKey() + tests := []struct { + name string + privKey *PrivKey + wantSignErr bool + wantVerifyPasses bool + }{ + {name: "valid sign-verify round", privKey: priv, wantSignErr: false, wantVerifyPasses: true}, + {name: "invalid private key", privKey: &PrivKey{Key: []byte{}}, wantSignErr: true, wantVerifyPasses: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.privKey.Sign(msg) + if tt.wantSignErr { + require.Error(t, err) + t.Logf("Got error: %s", err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + + pub := &PubKey{Key: tt.privKey.PubKey().Bytes()} + assert.Equal(t, tt.wantVerifyPasses, pub.VerifySignature(msg, got)) + }) + } +} diff --git a/crypto/keys/eth/secp256k1/secp256k1_nocgo.go b/crypto/keys/eth/secp256k1/secp256k1_nocgo.go new file mode 100644 index 00000000..ac6ad2e1 --- /dev/null +++ b/crypto/keys/eth/secp256k1/secp256k1_nocgo.go @@ -0,0 +1,47 @@ +//go:build !libsecp256k1_sdk +// +build !libsecp256k1_sdk + +package secp256k1 + +import ( + secp256k1 "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + + "github.com/cometbft/cometbft/crypto" +) + +// VerifyBytes verifies a signature of the form R || S. +// It rejects signatures which are not in lower-S form. +func (pubKey *PubKey) VerifySignature(msg []byte, sigStr []byte) bool { + if len(sigStr) != 64 { + return false + } + pub, err := secp256k1.ParsePubKey(pubKey.Key) + if err != nil { + return false + } + // parse the signature: + signature := signatureFromBytes(sigStr) + // Reject malleable signatures. libsecp256k1 does this check but btcec doesn't. + // see: https://github.com/ethereum/go-ethereum/blob/f9401ae011ddf7f8d2d95020b7446c17f8d98dc1/crypto/signature_nocgo.go#L90-L93 + // Serialize() would negate S value if it is over half order. + // Hence, if the signature is different after Serialize() if should be rejected. + modifiedSignature, parseErr := ecdsa.ParseDERSignature(signature.Serialize()) + if parseErr != nil { + return false + } + if !signature.IsEqual(modifiedSignature) { + return false + } + return signature.Verify(crypto.Sha256(msg), pub) +} + +// Read Signature struct from R || S. Caller needs to ensure +// that len(sigStr) == 64. +func signatureFromBytes(sigStr []byte) *ecdsa.Signature { + var r secp256k1.ModNScalar + r.SetByteSlice(sigStr[:32]) + var s secp256k1.ModNScalar + s.SetByteSlice(sigStr[32:64]) + return ecdsa.NewSignature(&r, &s) +} diff --git a/crypto/keys/eth/secp256k1/secp256k1_nocgo_test.go b/crypto/keys/eth/secp256k1/secp256k1_nocgo_test.go new file mode 100644 index 00000000..1941349c --- /dev/null +++ b/crypto/keys/eth/secp256k1/secp256k1_nocgo_test.go @@ -0,0 +1,50 @@ +//go:build !libsecp256k1_sdk +// +build !libsecp256k1_sdk + +package secp256k1 + +import ( + "testing" + + secp256k1 "github.com/btcsuite/btcd/btcec/v2" + "github.com/stretchr/testify/require" + + cosmoskey "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" +) + +// Ensure that signature verification works, and that +// non-canonical signatures fail. +// Note: run with CGO_ENABLED=0 or go test -tags !cgo. +func TestSignatureVerificationAndRejectUpperS(t *testing.T) { + msg := []byte("We have lingered long enough on the shores of the cosmic ocean.") + for i := 0; i < 500; i++ { + priv := cosmoskey.GenPrivKey() + sigStr, err := priv.Sign(msg) + require.NoError(t, err) + var r secp256k1.ModNScalar + r.SetByteSlice(sigStr[:32]) + var s secp256k1.ModNScalar + s.SetByteSlice(sigStr[32:64]) + require.False(t, s.IsOverHalfOrder()) + + pub := &PubKey{Key: priv.PubKey().Bytes()} + require.True(t, pub.VerifySignature(msg, sigStr)) + + // malleate: + var S256 secp256k1.ModNScalar + S256.SetByteSlice(secp256k1.S256().N.Bytes()) + s.Negate().Add(&S256) + require.True(t, s.IsOverHalfOrder()) + + rBytes := r.Bytes() + sBytes := s.Bytes() + malSigStr := make([]byte, 64) + copy(malSigStr[32-len(rBytes):32], rBytes[:]) + copy(malSigStr[64-len(sBytes):64], sBytes[:]) + require.False(t, pub.VerifySignature(msg, malSigStr), + "VerifyBytes incorrect with malleated & invalid S. sig=%v, key=%v", + malSigStr, + priv, + ) + } +} diff --git a/crypto/keys/eth/secp256k1/secp256k1_test.go b/crypto/keys/eth/secp256k1/secp256k1_test.go new file mode 100644 index 00000000..ed6f92cb --- /dev/null +++ b/crypto/keys/eth/secp256k1/secp256k1_test.go @@ -0,0 +1,203 @@ +package secp256k1_test + +import ( + "crypto/ecdsa" + "encoding/base64" + "encoding/hex" + "math/big" + "testing" + + btcSecp256k1 "github.com/btcsuite/btcd/btcec/v2" + btcecdsa "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/cometbft/cometbft/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + + ethsecp256k1 "github.com/initia-labs/initia/crypto/keys/eth/secp256k1" +) + +type keyData struct { + priv string + pub string + addr string +} + +var secpDataTable = []keyData{ + { + priv: "afdfd9c3d2095ef696594f6cedcae59e72dcd697e2a7521b1578140422a4f890", + pub: "02585b8820efe01a0cc841fefda079dbdc6471ccf51c4f4b86c9f9dc2ee46f2944", + addr: "06A85356DCb5b307096726FB86A78c59D38e08ee", + }, +} + +func TestPubKeySecp256k1Address(t *testing.T) { + for _, d := range secpDataTable { + privB, _ := hex.DecodeString(d.priv) + pubB, _ := hex.DecodeString(d.pub) + addrBbz, _ := hex.DecodeString(d.addr) + addrB := crypto.Address(addrBbz) + + priv := secp256k1.PrivKey{Key: privB} + + pubKey := ðsecp256k1.PubKey{Key: priv.PubKey().Bytes()} + + addr := pubKey.Address() + assert.Equal(t, pubKey, ðsecp256k1.PubKey{Key: pubB}, "Expected pub keys to match") + assert.Equal(t, addr, addrB, "Expected addresses to match") + } +} + +func TestSignAndValidateSecp256k1(t *testing.T) { + privKey := secp256k1.GenPrivKey() + pubKey := ðsecp256k1.PubKey{Key: privKey.PubKey().Bytes()} + + msg := crypto.CRandBytes(1000) + sig, err := privKey.Sign(msg) + require.Nil(t, err) + assert.True(t, pubKey.VerifySignature(msg, sig)) + + // ---- + // Test cross packages verification + msgHash := crypto.Sha256(msg) + btcPrivKey, btcPubKey := btcSecp256k1.PrivKeyFromBytes(privKey.Key) + // This fails: malformed signature: no header magic + // btcSig, err := secp256k1.ParseSignature(sig, secp256k1.S256()) + // require.NoError(t, err) + // assert.True(t, btcSig.Verify(msgHash, btcPubKey)) + // So we do a hacky way: + r := new(big.Int) + s := new(big.Int) + r.SetBytes(sig[:32]) + s.SetBytes(sig[32:]) + ok := ecdsa.Verify(btcPubKey.ToECDSA(), msgHash, r, s) + require.True(t, ok) + + sig2, err := btcecdsa.SignCompact(btcPrivKey, msgHash, false) + // Chop off compactSigRecoveryCode. + sig2 = sig2[1:] + require.NoError(t, err) + pubKey.VerifySignature(msg, sig2) + + // ---- + // Mutate the signature, just one bit. + sig[3] ^= byte(0x01) + assert.False(t, pubKey.VerifySignature(msg, sig)) +} + +// This test is intended to justify the removal of calls to the underlying library +// in creating the privkey. +func TestSecp256k1LoadPrivkeyAndSerializeIsIdentity(t *testing.T) { + numberOfTests := 256 + for i := 0; i < numberOfTests; i++ { + // Seed the test case with some random bytes + privKeyBytes := [32]byte{} + copy(privKeyBytes[:], crypto.CRandBytes(32)) + + // This function creates a private and public key in the underlying libraries format. + // The private key is basically calling new(big.Int).SetBytes(pk), which removes leading zero bytes + priv, _ := btcSecp256k1.PrivKeyFromBytes(privKeyBytes[:]) + // this takes the bytes returned by `(big int).Bytes()`, and if the length is less than 32 bytes, + // pads the bytes from the left with zero bytes. Therefore these two functions composed + // result in the identity function on privKeyBytes, hence the following equality check + // always returning true. + serializedBytes := priv.Serialize() + require.Equal(t, privKeyBytes[:], serializedBytes) + } +} + +func TestPubKeyEquals(t *testing.T) { + secp256K1PubKey := ðsecp256k1.PubKey{Key: secp256k1.GenPrivKey().PubKey().Bytes()} + + testCases := []struct { + msg string + pubKey cryptotypes.PubKey + other cryptotypes.PubKey + expectEq bool + }{ + { + "different bytes", + secp256K1PubKey, + ðsecp256k1.PubKey{Key: secp256k1.GenPrivKey().PubKey().Bytes()}, + false, + }, + { + "equals", + secp256K1PubKey, + ðsecp256k1.PubKey{ + Key: secp256K1PubKey.Key, + }, + true, + }, + { + "different types", + secp256K1PubKey, + ed25519.GenPrivKey().PubKey(), + false, + }, + } + + for _, tc := range testCases { + t.Run(tc.msg, func(t *testing.T) { + eq := tc.pubKey.Equals(tc.other) + require.Equal(t, eq, tc.expectEq) + }) + } +} + +func TestMarshalAmino(t *testing.T) { + aminoCdc := codec.NewLegacyAmino() + privKey := secp256k1.GenPrivKey() + pubKey := ðsecp256k1.PubKey{Key: privKey.PubKey().Bytes()} + + testCases := []struct { + desc string + msg codec.AminoMarshaler + typ interface{} + expBinary []byte + expJSON string + }{ + { + "secp256k1 private key", + privKey, + &secp256k1.PrivKey{}, + append([]byte{32}, privKey.Bytes()...), // Length-prefixed. + "\"" + base64.StdEncoding.EncodeToString(privKey.Bytes()) + "\"", + }, + { + "secp256k1 public key", + pubKey, + ðsecp256k1.PubKey{}, + append([]byte{33}, pubKey.Bytes()...), // Length-prefixed. + "\"" + base64.StdEncoding.EncodeToString(pubKey.Bytes()) + "\"", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + // Do a round trip of encoding/decoding binary. + bz, err := aminoCdc.Marshal(tc.msg) + require.NoError(t, err) + require.Equal(t, tc.expBinary, bz) + + err = aminoCdc.Unmarshal(bz, tc.typ) + require.NoError(t, err) + + require.Equal(t, tc.msg, tc.typ) + + // Do a round trip of encoding/decoding JSON. + bz, err = aminoCdc.MarshalJSON(tc.msg) + require.NoError(t, err) + require.Equal(t, tc.expJSON, string(bz)) + + err = aminoCdc.UnmarshalJSON(bz, tc.typ) + require.NoError(t, err) + + require.Equal(t, tc.msg, tc.typ) + }) + } +} diff --git a/proto/initia/crypto/eth/secp256k1/pubkey.proto b/proto/initia/crypto/eth/secp256k1/pubkey.proto new file mode 100644 index 00000000..89a110a7 --- /dev/null +++ b/proto/initia/crypto/eth/secp256k1/pubkey.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; +package initia.crypto.eth.secp256k1; + +import "amino/amino.proto"; +import "gogoproto/gogo.proto"; + +option go_package = "github.com/initia-labs/initia/crypto/keys/eth/secp256k1"; + +// PubKey defines a eth_secp256k1 public key +// Key is the compressed form of the pubkey. The first byte depends is a 0x02 byte +// if the y-coordinate is the lexicographically largest of the two associated with +// the x-coordinate. Otherwise the first byte is a 0x03. +// This prefix is followed with the x-coordinate. +message PubKey { + option (amino.name) = "initia/PubKeyEthSecp256k1"; + // The Amino encoding is simply the inner bytes field, and not the Amino + // encoding of the whole PubKey struct. + // + // Example (JSON): + // s := PubKey{Key: []byte{0x01}} + // out := AminoJSONEncoder(s) + // + // Then we have: + // out == `"MQ=="` + // out != `{"key":"MQ=="}` + option (amino.message_encoding) = "key_field"; + option (gogoproto.goproto_stringer) = false; + + bytes key = 1; +}