diff --git a/docs/architecture/adr-036-arbitrary-signature.md b/docs/architecture/adr-036-arbitrary-signature.md index d40929452e1c..afee478523af 100644 --- a/docs/architecture/adr-036-arbitrary-signature.md +++ b/docs/architecture/adr-036-arbitrary-signature.md @@ -75,7 +75,7 @@ Signed MsgSignData json example: "value": { "msg": [ { - "type": "sign/MsgSignData", + "type": "offchain/MsgSignData", "value": { "signer": "cosmos1hftz5ugqmpg9243xeegsqqav62f8hnywsjr4xr", "data": "cmFuZG9t" diff --git a/offchain/adr038/codec.go b/offchain/adr038/codec.go new file mode 100644 index 000000000000..5f25a1d3fd46 --- /dev/null +++ b/offchain/adr038/codec.go @@ -0,0 +1,27 @@ +package adr038 + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// ModuleCdc is the codec used by the module to serialize and deserialize data +var ModuleCdc = codec.NewAminoCodec(amino) +var amino = codec.NewLegacyAmino() + +// RegisterInterfaces adds offchain sdk.Msg types to the interface registry +func RegisterInterfaces(ir types.InterfaceRegistry) { + ir.RegisterImplementations((*sdk.Msg)(nil), &MsgSignData{}) +} + +// RegisterLegacyAminoCodec registers amino's legacy codec +func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { + cdc.RegisterConcrete(&MsgSignData{}, "offchain/MsgSignData", nil) +} + +func init() { + RegisterLegacyAminoCodec(amino) + cryptocodec.RegisterCrypto(amino) +} diff --git a/offchain/adr038/offchain.go b/offchain/adr038/offchain.go new file mode 100644 index 000000000000..8766d5efc0e6 --- /dev/null +++ b/offchain/adr038/offchain.go @@ -0,0 +1,74 @@ +package adr038 + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// interface implementation assertions +var _ msg = &MsgSignData{} + +const ( + // ExpectedChainID defines the chain id an off-chain message must have + ExpectedChainID = "" + // ExpectedAccountNumber defines the account number an off-chain message must have + ExpectedAccountNumber = 0 + // ExpectedSequence defines the sequence number an off-chain message must have + ExpectedSequence = 0 + // ExpectedRoute defines the route to use for sdk.Msg.ExpectedRoute() implementation for offchain messages + ExpectedRoute = "offchain" +) + +// msg defines an off-chain msg this exists so that offchain verification +// procedures are only applied to transactions lying in this package. +// TODO: making this exported would allow external types to use the SignatureVerifier and Signer +type msg interface { + sdk.Msg + offchain() +} + +// NewMsgSignData is MsgSignData's constructor +func NewMsgSignData(signer sdk.AccAddress, data []byte) *MsgSignData { + return &MsgSignData{ + Signer: signer.String(), + Data: data, + } +} + +// sdk.Msg implementation + +func (m *MsgSignData) Route() string { + return ExpectedRoute +} + +func (m *MsgSignData) Type() string { + return "MsgSignData" +} + +func (m *MsgSignData) ValidateBasic() error { + signer, err := sdk.AccAddressFromBech32(m.Signer) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "invalid signer: %s", err.Error()) + } + if signer.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "empty signer") + } + if len(m.Data) == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "empty data") + } + return nil +} + +func (m *MsgSignData) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(m)) +} + +func (m *MsgSignData) GetSigners() []sdk.AccAddress { + signer, err := sdk.AccAddressFromBech32(m.Signer) + if err != nil { + panic(err) + } + return []sdk.AccAddress{signer} +} + +func (m *MsgSignData) offchain() {} diff --git a/offchain/adr038/offchain.pb.go b/offchain/adr038/offchain.pb.go new file mode 100644 index 000000000000..44a68e3a598a --- /dev/null +++ b/offchain/adr038/offchain.pb.go @@ -0,0 +1,375 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: cosmos/offchain/adr038/offchain.proto + +package adr038 + +import ( + fmt "fmt" + _ "github.com/gogo/protobuf/gogoproto" + proto "github.com/gogo/protobuf/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 + +// MsgSignData defines an arbitrary, general-purpose, off-chain message +type MsgSignData struct { + // signer is the bech32 representation of the signer's account address + Signer string `protobuf:"bytes,1,opt,name=signer,proto3" json:"signer,omitempty"` + // data represents the raw bytes of the content that is signed (text, json, etc) + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` +} + +func (m *MsgSignData) Reset() { *m = MsgSignData{} } +func (m *MsgSignData) String() string { return proto.CompactTextString(m) } +func (*MsgSignData) ProtoMessage() {} +func (*MsgSignData) Descriptor() ([]byte, []int) { + return fileDescriptor_a277f2821243de96, []int{0} +} +func (m *MsgSignData) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgSignData) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgSignData.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 *MsgSignData) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgSignData.Merge(m, src) +} +func (m *MsgSignData) XXX_Size() int { + return m.Size() +} +func (m *MsgSignData) XXX_DiscardUnknown() { + xxx_messageInfo_MsgSignData.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgSignData proto.InternalMessageInfo + +func (m *MsgSignData) GetSigner() string { + if m != nil { + return m.Signer + } + return "" +} + +func (m *MsgSignData) GetData() []byte { + if m != nil { + return m.Data + } + return nil +} + +func init() { + proto.RegisterType((*MsgSignData)(nil), "cosmos.offchain.adr038.MsgSignData") +} + +func init() { + proto.RegisterFile("cosmos/offchain/adr038/offchain.proto", fileDescriptor_a277f2821243de96) +} + +var fileDescriptor_a277f2821243de96 = []byte{ + // 184 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x4d, 0xce, 0x2f, 0xce, + 0xcd, 0x2f, 0xd6, 0xcf, 0x4f, 0x4b, 0x4b, 0xce, 0x48, 0xcc, 0xcc, 0xd3, 0x4f, 0x4c, 0x29, 0x32, + 0x30, 0xb6, 0x80, 0xf3, 0xf5, 0x0a, 0x8a, 0xf2, 0x4b, 0xf2, 0x85, 0xc4, 0x20, 0xca, 0xf4, 0xe0, + 0xc2, 0x10, 0x65, 0x52, 0x22, 0xe9, 0xf9, 0xe9, 0xf9, 0x60, 0x25, 0xfa, 0x20, 0x16, 0x44, 0xb5, + 0x92, 0x25, 0x17, 0xb7, 0x6f, 0x71, 0x7a, 0x70, 0x66, 0x7a, 0x9e, 0x4b, 0x62, 0x49, 0xa2, 0x90, + 0x18, 0x17, 0x5b, 0x71, 0x66, 0x7a, 0x5e, 0x6a, 0x91, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0x67, 0x10, + 0x94, 0x27, 0x24, 0xc4, 0xc5, 0x92, 0x92, 0x58, 0x92, 0x28, 0xc1, 0xa4, 0xc0, 0xa8, 0xc1, 0x13, + 0x04, 0x66, 0x3b, 0xb9, 0x9d, 0x78, 0x24, 0xc7, 0x78, 0xe1, 0x91, 0x1c, 0xe3, 0x83, 0x47, 0x72, + 0x8c, 0x13, 0x1e, 0xcb, 0x31, 0x5c, 0x78, 0x2c, 0xc7, 0x70, 0xe3, 0xb1, 0x1c, 0x43, 0x94, 0x4e, + 0x7a, 0x66, 0x49, 0x46, 0x69, 0x92, 0x5e, 0x72, 0x7e, 0xae, 0x3e, 0xd4, 0xd1, 0x10, 0x4a, 0xb7, + 0x38, 0x25, 0x1b, 0xdd, 0xfd, 0x49, 0x6c, 0x60, 0x97, 0x18, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, + 0x79, 0x1f, 0xc7, 0xbb, 0xe0, 0x00, 0x00, 0x00, +} + +func (m *MsgSignData) 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 *MsgSignData) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgSignData) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Data) > 0 { + i -= len(m.Data) + copy(dAtA[i:], m.Data) + i = encodeVarintOffchain(dAtA, i, uint64(len(m.Data))) + i-- + dAtA[i] = 0x12 + } + if len(m.Signer) > 0 { + i -= len(m.Signer) + copy(dAtA[i:], m.Signer) + i = encodeVarintOffchain(dAtA, i, uint64(len(m.Signer))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintOffchain(dAtA []byte, offset int, v uint64) int { + offset -= sovOffchain(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *MsgSignData) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Signer) + if l > 0 { + n += 1 + l + sovOffchain(uint64(l)) + } + l = len(m.Data) + if l > 0 { + n += 1 + l + sovOffchain(uint64(l)) + } + return n +} + +func sovOffchain(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozOffchain(x uint64) (n int) { + return sovOffchain(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *MsgSignData) 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 ErrIntOverflowOffchain + } + 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: MsgSignData: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgSignData: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Signer", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowOffchain + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthOffchain + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthOffchain + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Signer = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Data", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowOffchain + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthOffchain + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthOffchain + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Data = append(m.Data[:0], dAtA[iNdEx:postIndex]...) + if m.Data == nil { + m.Data = []byte{} + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipOffchain(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthOffchain + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipOffchain(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, ErrIntOverflowOffchain + } + 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, ErrIntOverflowOffchain + } + 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, ErrIntOverflowOffchain + } + 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, ErrInvalidLengthOffchain + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupOffchain + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthOffchain + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthOffchain = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowOffchain = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupOffchain = fmt.Errorf("proto: unexpected end of group") +) diff --git a/offchain/adr038/sign.go b/offchain/adr038/sign.go new file mode 100644 index 000000000000..4ff5de2ec04b --- /dev/null +++ b/offchain/adr038/sign.go @@ -0,0 +1,162 @@ +package adr038 + +import ( + "errors" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" +) + +var ( + errNoTxConfig = errors.New("no tx config provided") + errNoKeyringProvided = errors.New("no keyring provided") +) + +// SignatureProvider offers an abstraction over private keys +// which can be constructed from raw private keys or keyrings +// it is a subset of the methods exposed by cryptotypes.PrivKey +type SignatureProvider interface { + Sign(msg []byte) (signedBytes []byte, err error) + PubKey() cryptotypes.PubKey +} + +// keyringWrapper implements SignatureProvider over a keyring +type keyringWrapper struct { + uid string + pubKey cryptotypes.PubKey + keyring keyring.Keyring +} + +func (k keyringWrapper) Sign(msg []byte) (signedBytes []byte, err error) { + signedBytes, _, err = k.keyring.Sign(k.uid, msg) + return +} + +func (k keyringWrapper) PubKey() cryptotypes.PubKey { + return k.pubKey +} + +func newKeyRingWrapper(uid string, keyring keyring.Keyring) (keyringWrapper, error) { + // assert from name exists + info, err := keyring.Key(uid) + if err != nil { + return keyringWrapper{}, err + } + pubKey, err := info.GetPubKey() + if err != nil { + return keyringWrapper{}, err + } + wrapper := keyringWrapper{uid: uid, pubKey: pubKey} + return wrapper, nil +} + +// NewSignerFromClientContext builds an offchain message signer from a client context +func NewSignerFromClientContext(clientCtx client.Context) (Signer, error) { + if clientCtx.TxConfig == nil { + return Signer{}, errNoTxConfig + } + if clientCtx.Keyring == nil { + return Signer{}, errNoKeyringProvided + } + privKey, err := newKeyRingWrapper(clientCtx.GetFromName(), clientCtx.Keyring) + if err != nil { + return Signer{}, err + } + return NewSigner(clientCtx.TxConfig, privKey), nil +} + +// NewSigner is Signer's constructor +func NewSigner(txConfig client.TxConfig, provider SignatureProvider) Signer { + return Signer{ + txConfig: txConfig, + privKey: provider, + } +} + +// Signer defines an offchain messages signer +type Signer struct { + txConfig client.TxConfig + privKey SignatureProvider +} + +// Sign produces a signed tx given a private key +// and the msgs we're aiming to sign +func (s Signer) Sign(msgs []sdk.Msg) (authsigning.SigVerifiableTx, error) { + if len(msgs) == 0 { + return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "no msg provided") + } + // build unsigned tx + builder := s.txConfig.NewTxBuilder() + + for i, msg := range msgs { + err := verifyMessage(msg) + if err != nil { + return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "message number %d is invalid: %s", i, err) + } + } + err := builder.SetMsgs(msgs...) + if err != nil { + return nil, err + } + + // prepare transaction to sign + signMode := s.txConfig.SignModeHandler().DefaultMode() + + signerData := authsigning.SignerData{ + ChainID: ExpectedChainID, + AccountNumber: ExpectedAccountNumber, + Sequence: ExpectedSequence, + } + + sigData := signing.SingleSignatureData{ + SignMode: signMode, + Signature: nil, + } + + sig := signing.SignatureV2{ + PubKey: s.privKey.PubKey(), + Data: &sigData, + Sequence: ExpectedSequence, + } + err = builder.SetSignatures(sig) + if err != nil { + return nil, err + } + + bytesToSign, err := s.txConfig.SignModeHandler(). + GetSignBytes( + signMode, + signerData, + builder.GetTx(), + ) + if err != nil { + return nil, err + } + + signedBytes, err := s.privKey.Sign(bytesToSign) + if err != nil { + return nil, err + } + + sigData = signing.SingleSignatureData{ + SignMode: signMode, + Signature: signedBytes, + } + sig = signing.SignatureV2{ + PubKey: s.privKey.PubKey(), + Data: &sigData, + Sequence: ExpectedSequence, + } + + err = builder.SetSignatures(sig) + if err != nil { + return nil, err + } + + return builder.GetTx(), nil +} diff --git a/offchain/adr038/sign_test.go b/offchain/adr038/sign_test.go new file mode 100644 index 000000000000..0f306c4ed11c --- /dev/null +++ b/offchain/adr038/sign_test.go @@ -0,0 +1,50 @@ +package adr038 + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +type signerTestSuite struct { + suite.Suite + signer Signer + verifier SignatureVerifier + address sdk.AccAddress +} + +func (ts *signerTestSuite) SetupTest() { + encConf := simapp.MakeTestEncodingConfig() + privKey := secp256k1.GenPrivKeyFromSecret(nil) + + RegisterInterfaces(encConf.InterfaceRegistry) + RegisterLegacyAminoCodec(encConf.Amino) + ts.signer = NewSigner(encConf.TxConfig, privKey) + ts.verifier = NewVerifier(encConf.TxConfig.SignModeHandler()) + ts.address = sdk.AccAddress(privKey.PubKey().Address()) +} + +func (ts *signerTestSuite) TestEmptyMsgs() { + _, err := ts.signer.Sign(nil) + ts.Require().True(errors.Is(err, sdkerrors.ErrInvalidRequest), "unexpected error: %s", err) +} + +// tests sign verify cycle works +func (ts *signerTestSuite) TestVerifyCompatibility() { + tx, err := ts.signer.Sign([]sdk.Msg{ + NewMsgSignData(ts.address, []byte("data")), + }) + ts.Require().NoError(err, "error while signing transaction") + err = ts.verifier.Verify(tx) + ts.Require().NoError(err, "valid transaction should be verified") +} + +func TestSigner(t *testing.T) { + suite.Run(t, new(signerTestSuite)) +} diff --git a/offchain/adr038/verify.go b/offchain/adr038/verify.go new file mode 100644 index 000000000000..c08bde8f83e0 --- /dev/null +++ b/offchain/adr038/verify.go @@ -0,0 +1,100 @@ +package adr038 + +import ( + "errors" + "fmt" + + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" +) + +// errors are private and used only for testing purposes +var ( + errInvalidType = errors.New("invalid type") + errInvalidTypeURL = errors.New("invalid type url") +) + +// NewVerifier is SignatureVerifier's constructor +func NewVerifier(signModeHandler authsigning.SignModeHandler) SignatureVerifier { + return SignatureVerifier{signModeHandler: signModeHandler} +} + +// SignatureVerifier takes care of verifying transactions given +// an instance of authsigning.SignModeHandler +type SignatureVerifier struct { + signModeHandler authsigning.SignModeHandler +} + +// Verify takes an sdk.Tx and verifies it +func (v SignatureVerifier) Verify(tx sdk.Tx) error { + sigTx, ok := tx.(authsigning.SigVerifiableTx) + if !ok { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "cannot verify tx of type %T", tx) + } + + msgs := sigTx.GetMsgs() + if len(msgs) == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "no message provided") + } + for i, msg := range msgs { + err := verifyMessage(msg) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "message number %d is invalid: %s", i, err) + } + } + + signers, err := sigTx.GetPubKeys() + if err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidPubKey, err.Error()) + } + + signatures, err := sigTx.GetSignaturesV2() + if err != nil { + return fmt.Errorf("cannot verify: %w", err) + } + if len(signatures) != len(signers) { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "signatures and signers mismatch: %d <-> %d", len(signers), len(signatures)) + } + for i, signature := range signatures { + err := verifySignature(tx, signature, signers[i], v.signModeHandler) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "invalid signature %d: %s", i, err) + } + } + return nil +} + +// verifySignature verifies a single signature +func verifySignature(tx sdk.Tx, sig signing.SignatureV2, signer cryptotypes.PubKey, handler authsigning.SignModeHandler) error { + // TODO: we're imposing chainID accountNumber and sequence, is there a way to verify those params beforehand? + // TODO: so we can return a bad request error instead of an unauthorized sig one + signerData := authsigning.SignerData{ + ChainID: ExpectedChainID, + AccountNumber: ExpectedAccountNumber, + Sequence: ExpectedSequence, + } + err := authsigning.VerifySignature(signer, signerData, sig.Data, handler, tx) + if err != nil { + return err + } + return nil +} + +// verifyMessage asserts that the message implementation fits offchain specification correctly +func verifyMessage(m sdk.Msg) error { + // ensure the sdk.msg messages are of type offchain.msg + // generally speaking we do not want to try to handle + // any other type of transaction aside from the offchain ones + // as they abide by different rules + _, valid := m.(msg) + if !valid { + return fmt.Errorf("%w: %T", errInvalidType, m) + } + if sdk.MsgTypeURL(m) != sdk.MsgTypeURL(&MsgSignData{}) { + return fmt.Errorf("%w: %s", errInvalidTypeURL, sdk.MsgTypeURL(m)) + } + return nil +} diff --git a/offchain/adr038/verify_test.go b/offchain/adr038/verify_test.go new file mode 100644 index 000000000000..f68b2ad60cc0 --- /dev/null +++ b/offchain/adr038/verify_test.go @@ -0,0 +1,70 @@ +package adr038 + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +type verifyMessageTestSuite struct { + suite.Suite + address sdk.AccAddress + validData []byte +} + +func (ts *verifyMessageTestSuite) TestValidMessage() { + err := verifyMessage(NewMsgSignData(ts.address, ts.validData)) + ts.Require().NoError(err, "message should be valid") +} + +func (ts *verifyMessageTestSuite) TestInvalidMessageType() { + err := verifyMessage(&types.MsgSend{}) + ts.Require().True(errors.Is(err, errInvalidType), "unexpected error: %s", err) +} + +func (ts *verifyMessageTestSuite) TestInvalidRoute() { + // err := verifyMessage() + // ts.Require().True(errors.Is(err, errInvalidRoute), "unexpected error: %s", err) +} + +type signatureVerifierSuite struct { + suite.Suite + verifier SignatureVerifier + txDecoder sdk.TxDecoder + invalidTx []byte +} + +func (ts *signatureVerifierSuite) SetupTest() { + encConf := simapp.MakeTestEncodingConfig() + RegisterInterfaces(encConf.InterfaceRegistry) + RegisterLegacyAminoCodec(encConf.Amino) + ts.txDecoder = encConf.TxConfig.TxJSONDecoder() + ts.invalidTx = []byte(`{"body":{"messages":[{"@type":"/cosmos.offchain.v1alpha1.MsgSignData","signer":"cosmos1346fyal5a9xxwlygkqmkkqf7g3q3zwzpdmkam8","data":"ZGF0YQ=="}],"memo":"","timeout_height":"0","extension_options":[],"non_critical_extension_options":[]},"auth_info":{"signer_infos":[{"public_key":{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A+FkzsHk5mVRk8IkVq5p0XapCrqu1MFf8KT594BtN6ss"},"mode_info":{"single":{"mode":"SIGN_MODE_DIRECT"}},"sequence":"0"}],"fee":{"amount":[],"gas_limit":"0","payer":"","granter":""}},"signatures":["Ftbdnj79Hms/JCJS8TkyoESuO1t7M0+A3fDCzPTHpchpzTE5qHRw8L0dKZYTO81ewQO7emRSyB2OshAdPlApHQ=="]}`) + ts.verifier = NewVerifier(encConf.TxConfig.SignModeHandler()) +} + +func (ts *signatureVerifierSuite) TestInvalidTxType() { + err := ts.verifier.Verify((sdk.FeeTx)(nil)) + ts.Suite.True(errors.Is(err, sdkerrors.ErrInvalidRequest), "unexpected error: %s", err) +} + +func (ts *signatureVerifierSuite) TestInvalidSignature() { + tx, err := ts.txDecoder(ts.invalidTx) + ts.Require().Nil(err, "decode of tx failed") + err = ts.verifier.Verify(tx) + ts.Suite.Require().True(errors.Is(sdkerrors.ErrUnauthorized, err), "unexpected error: %s", err) +} + +func TestVerifyMessage(t *testing.T) { + suite.Run(t, new(verifyMessageTestSuite)) +} + +func TestSignatureVerifier(t *testing.T) { + suite.Run(t, new(signatureVerifierSuite)) +} diff --git a/proto/cosmos/offchain/adr038/offchain.proto b/proto/cosmos/offchain/adr038/offchain.proto new file mode 100644 index 000000000000..e37a58ded68d --- /dev/null +++ b/proto/cosmos/offchain/adr038/offchain.proto @@ -0,0 +1,15 @@ +syntax="proto3"; + +package cosmos.offchain.adr038; + +option go_package="github.com/cosmos/cosmos-sdk/offchain/adr038"; + +import "gogoproto/gogo.proto"; + +// MsgSignData defines an arbitrary, general-purpose, off-chain message +message MsgSignData { + // signer is the bech32 representation of the signer's account address + string signer = 1; + // data represents the raw bytes of the content that is signed (text, json, etc) + bytes data = 2; +}