diff --git a/tx/docs.go b/tx/docs.go new file mode 100644 index 00000000..6a5ea3ce --- /dev/null +++ b/tx/docs.go @@ -0,0 +1,10 @@ +/* +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) +*/ +package tx diff --git a/tx/multi.go b/tx/multi.go new file mode 100644 index 00000000..eac86b01 --- /dev/null +++ b/tx/multi.go @@ -0,0 +1,67 @@ +package tx + +import ( + "github.com/pkg/errors" + crypto "github.com/tendermint/go-crypto" + data "github.com/tendermint/go-data" +) + +// MultiSig lets us wrap arbitrary data with a go-crypto signature +// +// TODO: rethink how we want to integrate this with KeyStore so it makes +// more sense (particularly the verify method) +type MultiSig struct { + Data data.Bytes + Sigs []Signed +} + +type Signed struct { + Sig crypto.SignatureS + Pubkey crypto.PubKeyS +} + +var _ SigInner = &MultiSig{} + +func NewMulti(data []byte) Sig { + return Sig{&MultiSig{Data: data}} +} + +// SignBytes returns the original data passed into `NewSig` +func (s *MultiSig) SignBytes() []byte { + return s.Data +} + +// 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 { + if pubkey == nil || sig == nil { + return errors.New("Signature or Key missing") + } + + // set the value once we are happy + x := Signed{crypto.SignatureS{sig}, crypto.PubKeyS{pubkey}} + s.Sigs = append(s.Sigs, x) + 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") + } + + keys := make([]crypto.PubKey, len(s.Sigs)) + for i := range s.Sigs { + ms := s.Sigs[i] + if !ms.Pubkey.VerifyBytes(s.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/tx/multi_test.go b/tx/multi_test.go new file mode 100644 index 00000000..815b9e15 --- /dev/null +++ b/tx/multi_test.go @@ -0,0 +1,77 @@ +package tx + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + crypto "github.com/tendermint/go-crypto" + keys "github.com/tendermint/go-keys" + "github.com/tendermint/go-keys/cryptostore" + "github.com/tendermint/go-keys/storage/memstorage" +) + +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 { + tx := NewMulti([]byte(tc.data)) + // unsigned version + _, err = tx.Signers() + assert.NotNil(err) + orig, err := tx.TxBytes() + require.Nil(err, "%+v", err) + data := tx.SignBytes() + assert.Equal(tc.data, string(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() + assert.Equal(tc.data, string(data)) + } +} diff --git a/tx/one.go b/tx/one.go new file mode 100644 index 00000000..0ad61dd5 --- /dev/null +++ b/tx/one.go @@ -0,0 +1,58 @@ +package tx + +import ( + "github.com/pkg/errors" + crypto "github.com/tendermint/go-crypto" + data "github.com/tendermint/go-data" +) + +// OneSig lets us wrap arbitrary data with a go-crypto signature +// +// TODO: rethink how we want to integrate this with KeyStore so it makes +// more sense (particularly the verify method) +type OneSig struct { + Data data.Bytes + Signed +} + +var _ SigInner = &OneSig{} + +func New(data []byte) Sig { + return WrapSig(&OneSig{Data: data}) +} + +// SignBytes returns the original data passed into `NewSig` +func (s *OneSig) SignBytes() []byte { + return s.Data +} + +// 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 { + if pubkey == nil || sig == nil { + return errors.New("Signature or Key missing") + } + if !s.Sig.Empty() { + return errors.New("Transaction can only be signed once") + } + + // set the value once we are happy + s.Pubkey = crypto.PubKeyS{pubkey} + s.Sig = crypto.SignatureS{sig} + 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.Pubkey.Empty() || s.Sig.Empty() { + return nil, errors.New("Never signed") + } + if !s.Pubkey.VerifyBytes(s.Data, s.Sig) { + return nil, errors.New("Signature doesn't match") + } + return []crypto.PubKey{s.Pubkey}, nil +} diff --git a/tx/one_test.go b/tx/one_test.go new file mode 100644 index 00000000..71b8011e --- /dev/null +++ b/tx/one_test.go @@ -0,0 +1,73 @@ +package tx + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + crypto "github.com/tendermint/go-crypto" + keys "github.com/tendermint/go-keys" + "github.com/tendermint/go-keys/cryptostore" + "github.com/tendermint/go-keys/storage/memstorage" +) + +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 { + tx := New([]byte(tc.data)) + // unsigned version + _, err = tx.Signers() + assert.NotNil(err) + orig, err := tx.TxBytes() + require.Nil(err, "%+v", err) + data := tx.SignBytes() + assert.Equal(tc.data, string(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() + assert.Equal(tc.data, string(data)) + } +} diff --git a/tx/reader.go b/tx/reader.go new file mode 100644 index 00000000..7689693c --- /dev/null +++ b/tx/reader.go @@ -0,0 +1,76 @@ +package tx + +import ( + crypto "github.com/tendermint/go-crypto" + data "github.com/tendermint/go-data" + keys "github.com/tendermint/go-keys" +) + +const ( + typeOneSig = byte(0x01) + typeMultiSig = byte(0x02) + nameOneSig = "sig" + nameMultiSig = "multi" +) + +var _ keys.Signable = Sig{} +var TxMapper data.Mapper + +func init() { + TxMapper = data.NewMapper(Sig{}). + RegisterInterface(&OneSig{}, nameOneSig, typeOneSig). + RegisterInterface(&MultiSig{}, nameMultiSig, typeMultiSig) +} + +/* +DO NOT USE this interface. + +It is public by necessity but should never be used directly +outside of this package. + +Only use Sig, never SigInner +*/ +type SigInner interface { + SignBytes() []byte + Sign(pubkey crypto.PubKey, sig crypto.Signature) error + Signers() ([]crypto.PubKey, error) +} + +// Sig is what is exported, and handles serialization +type Sig struct { + SigInner +} + +// TxBytes +func (s Sig) TxBytes() ([]byte, error) { + return data.ToWire(s) +} + +// WrapSig goes from concrete implementation to "interface" struct +func WrapSig(pk SigInner) Sig { + if wrap, ok := pk.(Sig); ok { + pk = wrap.Unwrap() + } + return Sig{pk} +} + +// Unwrap recovers the concrete interface safely (regardless of levels of embeds) +func (p Sig) Unwrap() SigInner { + pk := p.SigInner + for wrap, ok := pk.(Sig); ok; wrap, ok = pk.(Sig) { + pk = wrap.SigInner + } + return pk +} + +func (p Sig) MarshalJSON() ([]byte, error) { + return TxMapper.ToJSON(p.Unwrap()) +} + +func (p *Sig) UnmarshalJSON(data []byte) (err error) { + parsed, err := TxMapper.FromJSON(data) + if err == nil && parsed != nil { + p.SigInner = parsed.(SigInner) + } + return +} diff --git a/tx/reader_test.go b/tx/reader_test.go new file mode 100644 index 00000000..54b76af9 --- /dev/null +++ b/tx/reader_test.go @@ -0,0 +1,70 @@ +package tx + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + crypto "github.com/tendermint/go-crypto" + data "github.com/tendermint/go-data" + "github.com/tendermint/go-keys/cryptostore" + "github.com/tendermint/go-keys/storage/memstorage" +) + +func TestReader(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + algo := crypto.NameEd25519 + cstore := cryptostore.New( + cryptostore.SecretBox, + memstorage.New(), + ) + type sigs struct{ name, pass string } + u := sigs{"alice", "1234"} + u2 := sigs{"bob", "foobar"} + + _, err := cstore.Create(u.name, u.pass, algo) + require.Nil(err, "%+v", err) + _, err = cstore.Create(u2.name, u2.pass, algo) + require.Nil(err, "%+v", err) + + cases := []struct { + tx Sig + sigs []sigs + }{ + {New([]byte("first")), nil}, + {New([]byte("second")), []sigs{u}}, + {New([]byte("other")), []sigs{u2}}, + {NewMulti([]byte("m-first")), nil}, + {NewMulti([]byte("m-second")), []sigs{u}}, + {NewMulti([]byte("m-other")), []sigs{u, u2}}, + } + + for _, tc := range cases { + tx := tc.tx + + // make sure json serialization and loading works w/o sigs + var pre Sig + pjs, err := data.ToJSON(tx) + require.Nil(err, "%+v", err) + err = data.FromJSON(pjs, &pre) + require.Nil(err, "%+v", err) + assert.Equal(tx, pre) + + for _, s := range tc.sigs { + err = cstore.Sign(s.name, s.pass, tx) + require.Nil(err, "%+v", err) + } + + var post Sig + sjs, err := data.ToJSON(tx) + require.Nil(err, "%+v", err) + err = data.FromJSON(sjs, &post) + require.Nil(err, "%+v\n%s", err, string(sjs)) + assert.Equal(tx, post) + + if len(tc.sigs) > 0 { + assert.NotEqual(pjs, sjs, "%s\n ------ %s", string(pjs), string(sjs)) + } + } +}