Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

txnbuild: function to verify sep10 challenge tx #1576

Merged
merged 3 commits into from
Aug 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion txnbuild/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ file. This project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

* Add `Transaction.BuildChallengeTx` method for building [SEP-10](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md) challenge transaction.
* Add `BuildChallengeTx` function for building [SEP-10](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md) challenge transaction([#1466](https://github.com/stellar/go/issues/1466)).
* Add `VerifyChallengeTx` method for verifying [SEP-10](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md) challenge transaction([#1530](https://github.com/stellar/go/issues/1530)).
* Add `TransactionFromXDR` function for building `txnbuild.Transaction` struct from a base64 XDR transaction envelope[#1329](https://github.com/stellar/go/issues/1329).
* Fix bug that allowed multiple calls to `Transaction.Build` increment the number of operations in a transaction [#1448](https://github.com/stellar/go/issues/1448).
* Add `Transaction.SignWithKeyString` helper method for signing transactions using secret keys as strings.([#1564](https://github.com/stellar/go/issues/1564))
Expand Down
92 changes: 86 additions & 6 deletions txnbuild/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ func (tx *Transaction) BuildSignEncode(keypairs ...*keypair.Full) (string, error
}

// BuildChallengeTx is a factory method that creates a valid SEP 10 challenge, for use in web authentication.
// "timebound" is the time duration the transaction should be valid for, O means infinity.
// "timebound" is the time duration the transaction should be valid for.
// More details on SEP 10: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md
func BuildChallengeTx(serverSignerSecret, clientAccountID, anchorName, network string, timebound time.Duration) (string, error) {
serverKP, err := keypair.Parse(serverSignerSecret)
Expand Down Expand Up @@ -244,12 +244,12 @@ func BuildChallengeTx(serverSignerSecret, clientAccountID, anchorName, network s
AccountID: clientAccountID,
}

txTimebound := NewInfiniteTimeout()
if timebound > 0 {
currentTime := time.Now().UTC()
maxTime := currentTime.Add(timebound)
txTimebound = NewTimebounds(currentTime.Unix(), maxTime.Unix())
if timebound == 0 {
return "", errors.New("timebound cannot be 0")
}
currentTime := time.Now().UTC()
maxTime := currentTime.Add(timebound)
txTimebound := NewTimebounds(currentTime.Unix(), maxTime.Unix())

// Create a SEP 10 compatible response. See
// https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md#response
Expand Down Expand Up @@ -406,3 +406,83 @@ func (tx *Transaction) SignWithKeyString(keys ...string) error {

return tx.Sign(signers...)
}

// VerifyChallengeTx is a factory method that verifies a SEP 10 challenge transaction,
// for use in web authentication. It can be used by a server to verify that the challenge
// has been signed by the client.
// More details on SEP 10: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md
func VerifyChallengeTx(challengeTx, serverAccountID, network string) (bool, error) {
tx, err := TransactionFromXDR(challengeTx)
if err != nil {
return false, err
}
tx.Network = network

// verify transaction source
if tx.SourceAccount == nil {
return false, errors.New("transaction requires a source account")
}
if tx.SourceAccount.GetAccountID() != serverAccountID {
return false, errors.New("transaction source account is not equal to server's account")
}

// verify timebounds
if tx.Timebounds.MaxTime == TimeoutInfinite {
return false, errors.New("transaction requires non-infinite timebounds")
}
currentTime := time.Now().UTC().Unix()
if currentTime < tx.Timebounds.MinTime || currentTime > tx.Timebounds.MaxTime {
return false, errors.New("transaction is not within range of the specified timebounds")
}

// verify operation
if len(tx.Operations) != 1 {
return false, errors.New("transaction requires a single manage_data operation")
}
op, ok := tx.Operations[0].(*ManageData)
if !ok {
return false, errors.New("operation type should be manage_data")
}
if op.SourceAccount == nil {
return false, errors.New("operation should have a source account")
}
// verify signature from operation source
ok, err = verifyTxSignature(tx, op.SourceAccount.GetAccountID())
if err != nil {
return ok, err
}

// verify signature from server signing key
return verifyTxSignature(tx, serverAccountID)
}
abuiles marked this conversation as resolved.
Show resolved Hide resolved

// verifyTxSignature checks if a transaction has been signed by the provided Stellar account.
func verifyTxSignature(tx Transaction, accountID string) (bool, error) {
signerFound := false
txHash, err := tx.Hash()
if err != nil {
return signerFound, err
}

kp, err := keypair.Parse(accountID)
if err != nil {
return signerFound, err
}

// find and verify signatures
if tx.xdrEnvelope == nil {
return signerFound, errors.New("transaction has no signatures")
}
for _, s := range tx.xdrEnvelope.Signatures {
e := kp.Verify(txHash[:], s.Signature)
if e == nil {
signerFound = true
break
}
}

if !signerFound {
return signerFound, errors.Errorf("transaction not signed by %s", accountID)
}
return signerFound, nil
}
236 changes: 230 additions & 6 deletions txnbuild/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -740,17 +740,17 @@ func TestManageBuyOfferUpdateOffer(t *testing.T) {
func TestBuildChallengeTx(t *testing.T) {
kp0 := newKeypair0()

// infinite timebound
txeBase64, err := BuildChallengeTx(kp0.Seed(), kp0.Address(), "SDF", network.TestNetworkPassphrase, 0)
// 1 minute timebound
txeBase64, err := BuildChallengeTx(kp0.Seed(), kp0.Address(), "SDF", network.TestNetworkPassphrase, time.Minute)
assert.NoError(t, err)
var txXDR xdr.TransactionEnvelope
err = xdr.SafeUnmarshalBase64(txeBase64, &txXDR)
assert.NoError(t, err)
assert.Equal(t, xdr.SequenceNumber(0), txXDR.Tx.SeqNum, "sequence number should be 0")
assert.Equal(t, xdr.Uint32(100), txXDR.Tx.Fee, "Fee should be 100")
assert.Equal(t, 1, len(txXDR.Tx.Operations), "number operations should be 1")
assert.Equal(t, xdr.TimePoint(0), xdr.TimePoint(txXDR.Tx.TimeBounds.MinTime), "Min time should be 0")
assert.Equal(t, xdr.TimePoint(0), xdr.TimePoint(txXDR.Tx.TimeBounds.MaxTime), "Max time should be 0")
timeDiff := txXDR.Tx.TimeBounds.MaxTime - txXDR.Tx.TimeBounds.MinTime
assert.Equal(t, int64(60), int64(timeDiff), "time difference should be 300 seconds")
op := txXDR.Tx.Operations[0]
assert.Equal(t, xdr.OperationTypeManageData, op.Body.Type, "operation type should be manage data")
assert.Equal(t, xdr.String64("SDF auth"), op.Body.ManageDataOp.DataName, "DataName should be 'SDF auth'")
Expand All @@ -766,14 +766,19 @@ func TestBuildChallengeTx(t *testing.T) {
assert.Equal(t, xdr.Uint32(100), txXDR1.Tx.Fee, "Fee should be 100")
assert.Equal(t, 1, len(txXDR1.Tx.Operations), "number operations should be 1")

timeDiff := txXDR1.Tx.TimeBounds.MaxTime - txXDR1.Tx.TimeBounds.MinTime
timeDiff = txXDR1.Tx.TimeBounds.MaxTime - txXDR1.Tx.TimeBounds.MinTime
assert.Equal(t, int64(300), int64(timeDiff), "time difference should be 300 seconds")
op1 := txXDR1.Tx.Operations[0]
assert.Equal(t, xdr.OperationTypeManageData, op1.Body.Type, "operation type should be manage data")
assert.Equal(t, xdr.String64("SDF1 auth"), op1.Body.ManageDataOp.DataName, "DataName should be 'SDF1 auth'")
assert.Equal(t, 64, len(*op1.Body.ManageDataOp.DataValue), "DataValue should be 64 bytes")
}

//transaction with infinite timebound
_, err = BuildChallengeTx(kp0.Seed(), kp0.Address(), "sdf", network.TestNetworkPassphrase, 0)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "timebound cannot be 0")
}
}
func TestHashHex(t *testing.T) {
kp0 := newKeypair0()
sourceAccount := NewSimpleAccount(kp0.Address(), int64(9605939170639897))
Expand Down Expand Up @@ -1156,3 +1161,222 @@ func TestSignWithSecretKey(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, expected, actual, "base64 xdr should match")
}

func TestVerifyTxSignatureUnsignedTx(t *testing.T) {
kp0 := newKeypair0()
kp1 := newKeypair1()
txSource := NewSimpleAccount(kp0.Address(), int64(9605939170639897))
opSource := NewSimpleAccount(kp1.Address(), 0)
createAccount := CreateAccount{
Destination: "GCCOBXW2XQNUSL467IEILE6MMCNRR66SSVL4YQADUNYYNUVREF3FIV2Z",
Amount: "10",
SourceAccount: &opSource,
}
tx := Transaction{
SourceAccount: &txSource,
Operations: []Operation{&createAccount},
Timebounds: NewInfiniteTimeout(),
Network: network.TestNetworkPassphrase,
}

// verify unsigned tx
poliha marked this conversation as resolved.
Show resolved Hide resolved
err := tx.Build()
assert.NoError(t, err)
ok, err := verifyTxSignature(tx, kp0.Address())
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "transaction not signed by GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3")
}
assert.Equal(t, false, ok)
}

func TestVerifyTxSignatureSingle(t *testing.T) {
kp0 := newKeypair0()
kp1 := newKeypair1()
txSource := NewSimpleAccount(kp0.Address(), int64(9605939170639897))
opSource := NewSimpleAccount(kp1.Address(), 0)
createAccount := CreateAccount{
Destination: "GCCOBXW2XQNUSL467IEILE6MMCNRR66SSVL4YQADUNYYNUVREF3FIV2Z",
Amount: "10",
SourceAccount: &opSource,
}
tx := Transaction{
SourceAccount: &txSource,
Operations: []Operation{&createAccount},
Timebounds: NewInfiniteTimeout(),
Network: network.TestNetworkPassphrase,
}
// verify tx with one signature
err := tx.Build()
assert.NoError(t, err)
err = tx.Sign(kp0)
assert.NoError(t, err)
ok, err := verifyTxSignature(tx, kp0.Address())
assert.NoError(t, err)
assert.Equal(t, true, ok)
}

func TestVerifyTxSignatureMultiple(t *testing.T) {
kp0 := newKeypair0()
kp1 := newKeypair1()
txSource := NewSimpleAccount(kp0.Address(), int64(9605939170639897))
opSource := NewSimpleAccount(kp1.Address(), 0)
createAccount := CreateAccount{
Destination: "GCCOBXW2XQNUSL467IEILE6MMCNRR66SSVL4YQADUNYYNUVREF3FIV2Z",
Amount: "10",
SourceAccount: &opSource,
}
tx := Transaction{
SourceAccount: &txSource,
Operations: []Operation{&createAccount},
Timebounds: NewInfiniteTimeout(),
Network: network.TestNetworkPassphrase,
}
// verify tx with multiple signature
err := tx.Build()
assert.NoError(t, err)
err = tx.Sign(kp0, kp1)
assert.NoError(t, err)
ok, err := verifyTxSignature(tx, kp0.Address())
assert.NoError(t, err)
assert.Equal(t, true, ok)
ok, err = verifyTxSignature(tx, kp1.Address())
assert.NoError(t, err)
assert.Equal(t, true, ok)
}
func TestVerifyTxSignatureInvalid(t *testing.T) {
kp0 := newKeypair0()
kp1 := newKeypair1()
txSource := NewSimpleAccount(kp0.Address(), int64(9605939170639897))
opSource := NewSimpleAccount(kp1.Address(), 0)
createAccount := CreateAccount{
Destination: "GCCOBXW2XQNUSL467IEILE6MMCNRR66SSVL4YQADUNYYNUVREF3FIV2Z",
Amount: "10",
SourceAccount: &opSource,
}
tx := Transaction{
SourceAccount: &txSource,
Operations: []Operation{&createAccount},
Timebounds: NewInfiniteTimeout(),
Network: network.TestNetworkPassphrase,
}
// verify invalid signer
err := tx.Build()
assert.NoError(t, err)
err = tx.Sign(kp0, kp1)
assert.NoError(t, err)
ok, err := verifyTxSignature(tx, "GATBMIXTHXYKSUZSZUEJKACZ2OS2IYUWP2AIF3CA32PIDLJ67CH6Y5UY")
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "transaction not signed by GATBMIXTHXYKSUZSZUEJKACZ2OS2IYUWP2AIF3CA32PIDLJ67CH6Y5UY")
}
assert.Equal(t, false, ok)
}

func TestVerifyChallengeTxInvalid(t *testing.T) {
invalidTx := "AAAAACYWIvM98KlTMs0IlQBZ06WkYpZ+gILsQN6ega0++I/sAAAAZAAXeEkAAAABAAAAAAAAAAEAAAAQMkExVjZKNTcwM0c0N1hIWQAAAAEAAAABAAAAACYWIvM98KlTMs0IlQBZ06WkYpZ+gILsQN6ega0++I/sAAAAAQAAAADMSEvcRKXsaUNna++Hy7gWm/CfqTjEA7xoGypfrFGUHAAAAAAAAAACCPHRAAAAAAAAAAABPviP7AAAAEBu6BCKf4WZHPum5+29Nxf6SsJNN8bgjp1+e1uNBaHjRg3rdFZYgUqEqbHxVEs7eze3IeRbjMZxS3zPf/xwJCEI"

isValid, err := VerifyChallengeTx(invalidTx, "GATBMIXTHXYKSUZSZUEJKACZ2OS2IYUWP2AIF3CA32PIDLJ67CH6Y5UY", network.TestNetworkPassphrase)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "transaction requires non-infinite timebounds")
}
assert.Equal(t, false, isValid, "challenge should not be valid")
}

func TestVerifyChallengeTxInvalidTimebound(t *testing.T) {
kp0 := newKeypair0()
kp1 := newKeypair1()

// transaction with elapsed timebound
poliha marked this conversation as resolved.
Show resolved Hide resolved
newChallenge, err := BuildChallengeTx(kp0.Seed(), kp1.Address(), "sdf", network.TestNetworkPassphrase, 1)
assert.NoError(t, err)
time.Sleep(2 * time.Second)
isValid, err := VerifyChallengeTx(newChallenge, kp0.Address(), network.TestNetworkPassphrase)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "transaction is not within range of the specified timebounds")
}
assert.Equal(t, false, isValid, "challenge should not be valid")
}

func TestVerifyChallengeTxNotSigned(t *testing.T) {
kp0 := newKeypair0()
kp1 := newKeypair1()

// transaction not signed by client
newChallenge, err := BuildChallengeTx(kp0.Seed(), kp1.Address(), "sdf", network.TestNetworkPassphrase, 300)
assert.NoError(t, err)
isValid, err := VerifyChallengeTx(newChallenge, kp0.Address(), network.TestNetworkPassphrase)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "transaction not signed by "+kp1.Address())
}
assert.Equal(t, false, isValid, "challenge should not be valid")
}

func TestVerifyChallengeTxSigned(t *testing.T) {
kp0 := newKeypair0()
kp1 := newKeypair1()

// valid transaction signed by client
newChallenge, err := BuildChallengeTx(kp0.Seed(), kp1.Address(), "sdf", network.TestNetworkPassphrase, 300)
assert.NoError(t, err)
newTx, err := TransactionFromXDR(newChallenge)
assert.NoError(t, err)
newTx.Network = network.TestNetworkPassphrase
err = newTx.Sign(kp1)
assert.NoError(t, err)
newChallenge, err = newTx.Base64()
assert.NoError(t, err)
isValid, err := VerifyChallengeTx(newChallenge, kp0.Address(), network.TestNetworkPassphrase)
assert.NoError(t, err)
assert.Equal(t, true, isValid, "challenge should be valid")
}

func TestVerifyChallengeTxInvalidOp(t *testing.T) {
kp0 := newKeypair0()
kp1 := newKeypair1()

// invalid operation type
txSource := NewSimpleAccount(kp0.Address(), 0)
opSource := NewSimpleAccount(kp1.Address(), 0)
createAccount := CreateAccount{
Destination: "GCCOBXW2XQNUSL467IEILE6MMCNRR66SSVL4YQADUNYYNUVREF3FIV2Z",
Amount: "10",
SourceAccount: &opSource,
}
newTx := Transaction{
SourceAccount: &txSource,
Operations: []Operation{&createAccount},
Timebounds: NewTimeout(300),
Network: network.TestNetworkPassphrase,
}
err := newTx.Build()
assert.NoError(t, err)
err = newTx.Sign(kp0, kp1)
assert.NoError(t, err)
newChallenge, err := newTx.Base64()
assert.NoError(t, err)
isValid, err := VerifyChallengeTx(newChallenge, kp0.Address(), network.TestNetworkPassphrase)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "operation type should be manage_data")
}
assert.Equal(t, false, isValid, "challenge should be invalid")
}

func TestVerifyChallengeTxInvalidSource(t *testing.T) {
kp0 := newKeypair0()
kp1 := newKeypair1()

// transaction with invalid source
newChallenge, err := BuildChallengeTx(kp1.Seed(), kp1.Address(), "sdf", network.TestNetworkPassphrase, 300)
assert.NoError(t, err)
newTx, err := TransactionFromXDR(newChallenge)
assert.NoError(t, err)
newTx.Network = network.TestNetworkPassphrase
err = newTx.Sign(kp1)
assert.NoError(t, err)
newChallenge, err = newTx.Base64()
assert.NoError(t, err)
isValid, err := VerifyChallengeTx(newChallenge, kp0.Address(), network.TestNetworkPassphrase)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "transaction source account is not equal to server's account")
}
assert.Equal(t, false, isValid, "challenge should be valid")
}