From 24802303e02118ff232186a2a4ff57de0e49f659 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 17 May 2017 21:20:08 +0200 Subject: [PATCH] Start tx framework, testing sigs --- txs/base.go | 38 +++++++++++ txs/sigs.go | 171 +++++++++++++++++++++++++++++++++++++++++++++++ txs/sigs_test.go | 156 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 365 insertions(+) create mode 100644 txs/base.go create mode 100644 txs/sigs.go create mode 100644 txs/sigs_test.go diff --git a/txs/base.go b/txs/base.go new file mode 100644 index 0000000000..235e4b855c --- /dev/null +++ b/txs/base.go @@ -0,0 +1,38 @@ +package txs + +import ( + "github.com/tendermint/basecoin" + "github.com/tendermint/go-wire/data" +) + +const ( + // for utils... + ByteRaw = 0x1 + ByteFees = 0x2 + ByteMulti = 0x3 + + // for signatures + ByteSig = 0x16 + ByteMultiSig = 0x17 +) + +const ( + // for utils... + TypeRaw = "raw" + TypeFees = "fee" + TypeMulti = "multi" + + // for signatures + TypeSig = "sig" + TypeMultiSig = "multisig" +) + +// let's register data.Bytes as a "raw" tx, for tests or +// other data we don't want to post.... +func init() { + basecoin.TxMapper.RegisterImplementation(data.Bytes{}, TypeRaw, ByteRaw) +} + +func WrapBytes(d []byte) basecoin.Tx { + return basecoin.Tx{data.Bytes(d)} +} diff --git a/txs/sigs.go b/txs/sigs.go new file mode 100644 index 0000000000..c8ed07ac0b --- /dev/null +++ b/txs/sigs.go @@ -0,0 +1,171 @@ +/* +package tx contains generic Signable implementations that can be used +by your application or tests to handle authentication needs. + +It currently supports transaction data as opaque bytes and either single +or multiple private key signatures using straightforward algorithms. +It currently does not support N-of-M key share signing of other more +complex algorithms (although it would be great to add them). + +You can create them with NewSig() and NewMultiSig(), and they fulfill +the keys.Signable interface. You can then .Wrap() them to create +a basecoin.Tx. +*/ +package txs + +import ( + "github.com/pkg/errors" + + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/go-crypto/keys" + "github.com/tendermint/go-wire/data" + + "github.com/tendermint/basecoin" +) + +// Signed holds one signature of the data +type Signed struct { + Sig crypto.Signature + Pubkey crypto.PubKey +} + +func (s Signed) Empty() bool { + return s.Sig.Empty() || s.Pubkey.Empty() +} + +/**** Registration ****/ + +func init() { + basecoin.TxMapper. + RegisterImplementation(&OneSig{}, TypeSig, ByteSig). + RegisterImplementation(&MultiSig{}, TypeMultiSig, ByteMultiSig) +} + +/**** One Sig ****/ + +// OneSig lets us wrap arbitrary data with a go-crypto signature +type OneSig struct { + Tx basecoin.Tx `json:"tx"` + Signed `json:"signature"` +} + +var _ keys.Signable = &OneSig{} + +func NewSig(tx basecoin.Tx) *OneSig { + return &OneSig{Tx: tx} +} + +func (s *OneSig) Wrap() basecoin.Tx { + return basecoin.Tx{s} +} + +// TxBytes returns the full data with signatures +func (s *OneSig) TxBytes() ([]byte, error) { + return data.ToWire(s) +} + +// SignBytes returns the original data passed into `NewSig` +func (s *OneSig) SignBytes() []byte { + res, err := data.ToWire(s.Tx) + if err != nil { + panic(err) + } + return res +} + +// Sign will add a signature and pubkey. +// +// Depending on the Signable, one may be able to call this multiple times for multisig +// Returns error if called with invalid data or too many times +func (s *OneSig) Sign(pubkey crypto.PubKey, sig crypto.Signature) error { + signed := Signed{sig, pubkey} + if signed.Empty() { + return errors.New("Signature or Key missing") + } + if !s.Empty() { + return errors.New("Transaction can only be signed once") + } + // set the value once we are happy + s.Signed = signed + return nil +} + +// Signers will return the public key(s) that signed if the signature +// is valid, or an error if there is any issue with the signature, +// including if there are no signatures +func (s *OneSig) Signers() ([]crypto.PubKey, error) { + if s.Empty() { + return nil, errors.New("Never signed") + } + if !s.Pubkey.VerifyBytes(s.SignBytes(), s.Sig) { + return nil, errors.New("Signature doesn't match") + } + return []crypto.PubKey{s.Pubkey}, nil +} + +/**** MultiSig ****/ + +// MultiSig lets us wrap arbitrary data with a go-crypto signature +type MultiSig struct { + Tx basecoin.Tx `json:"tx"` + Sigs []Signed `json:"signatures"` +} + +var _ keys.Signable = &MultiSig{} + +func NewMulti(tx basecoin.Tx) *MultiSig { + return &MultiSig{Tx: tx} +} + +func (s *MultiSig) Wrap() basecoin.Tx { + return basecoin.Tx{s} +} + +// TxBytes returns the full data with signatures +func (s *MultiSig) TxBytes() ([]byte, error) { + return data.ToWire(s) +} + +// SignBytes returns the original data passed into `NewSig` +func (s *MultiSig) SignBytes() []byte { + res, err := data.ToWire(s.Tx) + if err != nil { + panic(err) + } + return res +} + +// Sign will add a signature and pubkey. +// +// Depending on the Signable, one may be able to call this multiple times for multisig +// Returns error if called with invalid data or too many times +func (s *MultiSig) Sign(pubkey crypto.PubKey, sig crypto.Signature) error { + signed := Signed{sig, pubkey} + if signed.Empty() { + return errors.New("Signature or Key missing") + } + // set the value once we are happy + s.Sigs = append(s.Sigs, signed) + return nil +} + +// Signers will return the public key(s) that signed if the signature +// is valid, or an error if there is any issue with the signature, +// including if there are no signatures +func (s *MultiSig) Signers() ([]crypto.PubKey, error) { + if len(s.Sigs) == 0 { + return nil, errors.New("Never signed") + } + // verify all the signatures before returning them + keys := make([]crypto.PubKey, len(s.Sigs)) + data := s.SignBytes() + for i := range s.Sigs { + ms := s.Sigs[i] + if !ms.Pubkey.VerifyBytes(data, ms.Sig) { + return nil, errors.Errorf("Signature %d doesn't match (key: %X)", i, ms.Pubkey.Bytes()) + } + keys[i] = ms.Pubkey + } + + return keys, nil +} diff --git a/txs/sigs_test.go b/txs/sigs_test.go new file mode 100644 index 0000000000..f0a5a2c994 --- /dev/null +++ b/txs/sigs_test.go @@ -0,0 +1,156 @@ +package txs + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/tendermint/basecoin" + crypto "github.com/tendermint/go-crypto" + keys "github.com/tendermint/go-crypto/keys" + "github.com/tendermint/go-crypto/keys/cryptostore" + "github.com/tendermint/go-crypto/keys/storage/memstorage" + wire "github.com/tendermint/go-wire" + "github.com/tendermint/go-wire/data" +) + +func checkSignBytes(t *testing.T, bytes []byte, expected string) { + // load it back... unwrap the tx + var preTx basecoin.Tx + err := wire.ReadBinaryBytes(bytes, &preTx) + require.Nil(t, err) + + // now make sure this tx is data.Bytes with the info we want + byt, ok := preTx.Unwrap().(data.Bytes) + require.True(t, ok) + assert.Equal(t, expected, string(byt)) +} + +func TestOneSig(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + algo := crypto.NameEd25519 + cstore := cryptostore.New( + cryptostore.SecretBox, + memstorage.New(), + ) + n, p := "foo", "bar" + n2, p2 := "other", "thing" + + acct, err := cstore.Create(n, p, algo) + require.Nil(err, "%+v", err) + acct2, err := cstore.Create(n2, p2, algo) + require.Nil(err, "%+v", err) + + cases := []struct { + data string + key keys.Info + name, pass string + }{ + {"first", acct, n, p}, + {"kehfkhefy8y", acct, n, p}, + {"second", acct2, n2, p2}, + } + + for _, tc := range cases { + inner := WrapBytes([]byte(tc.data)) + tx := NewSig(inner) + // unsigned version + _, err = tx.Signers() + assert.NotNil(err) + orig, err := tx.TxBytes() + require.Nil(err, "%+v", err) + data := tx.SignBytes() + checkSignBytes(t, data, tc.data) + + // sign it + err = cstore.Sign(tc.name, tc.pass, tx) + require.Nil(err, "%+v", err) + // but not twice + err = cstore.Sign(tc.name, tc.pass, tx) + require.NotNil(err) + + // make sure it is proper now + sigs, err := tx.Signers() + require.Nil(err, "%+v", err) + if assert.Equal(1, len(sigs)) { + // This must be refactored... + assert.Equal(tc.key.PubKey, sigs[0]) + } + // the tx bytes should change after this + after, err := tx.TxBytes() + require.Nil(err, "%+v", err) + assert.NotEqual(orig, after, "%X != %X", orig, after) + + // sign bytes are the same + data = tx.SignBytes() + checkSignBytes(t, data, tc.data) + } +} + +func TestMultiSig(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + algo := crypto.NameEd25519 + cstore := cryptostore.New( + cryptostore.SecretBox, + memstorage.New(), + ) + n, p := "foo", "bar" + n2, p2 := "other", "thing" + + acct, err := cstore.Create(n, p, algo) + require.Nil(err, "%+v", err) + acct2, err := cstore.Create(n2, p2, algo) + require.Nil(err, "%+v", err) + + type signer struct { + key keys.Info + name, pass string + } + cases := []struct { + data string + signers []signer + }{ + {"one", []signer{{acct, n, p}}}, + {"two", []signer{{acct2, n2, p2}}}, + {"both", []signer{{acct, n, p}, {acct2, n2, p2}}}, + } + + for _, tc := range cases { + inner := WrapBytes([]byte(tc.data)) + tx := NewMulti(inner) + // unsigned version + _, err = tx.Signers() + assert.NotNil(err) + orig, err := tx.TxBytes() + require.Nil(err, "%+v", err) + data := tx.SignBytes() + checkSignBytes(t, data, tc.data) + + // sign it + for _, s := range tc.signers { + err = cstore.Sign(s.name, s.pass, tx) + require.Nil(err, "%+v", err) + } + + // make sure it is proper now + sigs, err := tx.Signers() + require.Nil(err, "%+v", err) + if assert.Equal(len(tc.signers), len(sigs)) { + for i := range sigs { + // This must be refactored... + assert.Equal(tc.signers[i].key.PubKey, sigs[i]) + } + } + // the tx bytes should change after this + after, err := tx.TxBytes() + require.Nil(err, "%+v", err) + assert.NotEqual(orig, after, "%X != %X", orig, after) + + // sign bytes are the same + data = tx.SignBytes() + checkSignBytes(t, data, tc.data) + } +}