@ -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 |
@ -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 | |||||
} |
@ -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)) | |||||
} | |||||
} |
@ -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 | |||||
} |
@ -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)) | |||||
} | |||||
} |
@ -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 | |||||
} |
@ -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)) | |||||
} | |||||
} | |||||
} |