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: SEP 10 challenge builder #1468

Merged
merged 9 commits into from
Jul 16, 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
5 changes: 5 additions & 0 deletions txnbuild/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
All notable changes to this project will be documented in this
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.


## [v1.3.0](https://github.com/stellar/go/releases/tag/horizonclient-v1.3.0) - 2019-07-08

* Add support for getting the hex-encoded transaction hash with `Transaction.HashHex` method.
Expand Down
14 changes: 14 additions & 0 deletions txnbuild/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package txnbuild

import (
"fmt"
"time"

"github.com/stellar/go/keypair"
"github.com/stellar/go/network"
Expand Down Expand Up @@ -502,3 +503,16 @@ func ExampleManageBuyOffer() {
// Output: AAAAAH4RyzTWNfXhqwLUoCw91aWkZtgIzY8SAVkIPc0uFVmYAAAAZAAMoj8AAAAEAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAMAAAAAAAAAAFBQkNEAAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAADuaygAAAAABAAAAZAAAAAAAAAAAAAAAAAAAAAEuFVmYAAAAQPh8h1TrzDpcgzB/VE8V0X2pFGV8/JyuYrx0I5bRfBJuLJr0l8yL1isP1wZjvMdX7fNiktwSLuUuj749nWA6wAo=

}

func ExampleBuildChallengeTx() {
// Generate random nonce
serverSignerSeed := "SBZVMB74Z76QZ3ZOY7UTDFYKMEGKW5XFJEB6PFKBF4UYSSWHG4EDH7PY"
clientAccountID := "GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3"
anchorName := "SDF"
timebound := time.Duration(5 * time.Minute)

tx, err := BuildChallengeTx(serverSignerSeed, clientAccountID, anchorName, network.TestNetworkPassphrase, timebound)
_, err = checkChallengeTx(tx, anchorName)

check(err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a simple sanity check (if tx contains manage_data operation with a correct name) would be great to have.

}
15 changes: 15 additions & 0 deletions txnbuild/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"testing"

"github.com/stellar/go/keypair"
"github.com/stellar/go/support/errors"
"github.com/stellar/go/xdr"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -42,3 +44,16 @@ func check(err error) {
panic(err)
}
}

func checkChallengeTx(txeBase64, anchorName string) (bool, error) {
var txXDR xdr.TransactionEnvelope
err := xdr.SafeUnmarshalBase64(txeBase64, &txXDR)
if err != nil {
return false, err
}
op := txXDR.Tx.Operations[0]
if (xdr.OperationTypeManageData == op.Body.Type) && (op.Body.ManageDataOp.DataName == xdr.String64(anchorName+" auth")) {
return true, nil
}
return false, errors.New("invalid challenge tx")
}
76 changes: 76 additions & 0 deletions txnbuild/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ package txnbuild

import (
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"time"

"github.com/stellar/go/keypair"
"github.com/stellar/go/network"
Expand Down Expand Up @@ -198,6 +200,80 @@ func (tx *Transaction) BuildSignEncode(keypairs ...*keypair.Full) (string, error
return txeBase64, err
}

// 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.
// 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)
if err != nil {
return "", err
}

randomNonce, err := generateRandomNonce(64)
if err != nil {
return "", err
}

if len(randomNonce) != 64 {
return "", errors.New("64 byte long random nonce required")
}

// represent server signing account as SimpleAccount
sa := SimpleAccount{
AccountID: serverKP.Address(),
// Action needed in release: v2.0.0
// TODO: remove this and use "Sequence: 0" and build transaction with optional argument
// (https://github.com/stellar/go/issues/1259)
Sequence: int64(-1),
}

// represent client account as SimpleAccount
ca := SimpleAccount{
AccountID: clientAccountID,
}

txTimebound := NewInfiniteTimeout()
if timebound > 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
tx := Transaction{
SourceAccount: &sa,
Operations: []Operation{
&ManageData{
SourceAccount: &ca,
Name: anchorName + " auth",
Value: randomNonce,
},
},
Timebounds: txTimebound,
Network: network,
BaseFee: uint32(100),
}

txeB64, err := tx.BuildSignEncode(serverKP.(*keypair.Full))
if err != nil {
return "", err
}
return txeB64, nil
poliha marked this conversation as resolved.
Show resolved Hide resolved
}

// generateRandomNonce creates a cryptographically secure random slice of `n` bytes.
func generateRandomNonce(n int) ([]byte, error) {
bytes := make([]byte, n)
_, err := rand.Read(bytes)

if err != nil {
return []byte{}, err
}

return bytes, err
}

// HashHex returns the hex-encoded hash of the transaction.
func (tx *Transaction) HashHex() (string, error) {
hashByte, err := tx.Hash()
Expand Down
40 changes: 40 additions & 0 deletions txnbuild/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package txnbuild
import (
"crypto/sha256"
"testing"
"time"

"github.com/stellar/go/network"
"github.com/stellar/go/strkey"
Expand Down Expand Up @@ -736,6 +737,44 @@ func TestManageBuyOfferUpdateOffer(t *testing.T) {
assert.Equal(t, expected, received, "Base 64 XDR should match")
}

func TestBuildChallengeTx(t *testing.T) {
kp0 := newKeypair0()

// infinite timebound
txeBase64, err := BuildChallengeTx(kp0.Seed(), kp0.Address(), "SDF", network.TestNetworkPassphrase, 0)
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")
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'")
assert.Equal(t, 64, len(*op.Body.ManageDataOp.DataValue), "DataValue should be 64 bytes")

// 5 minutes timebound
txeBase64, err = BuildChallengeTx(kp0.Seed(), kp0.Address(), "SDF1", network.TestNetworkPassphrase, time.Duration(5*time.Minute))
assert.NoError(t, err)

var txXDR1 xdr.TransactionEnvelope
err = xdr.SafeUnmarshalBase64(txeBase64, &txXDR1)
assert.NoError(t, err)
assert.Equal(t, xdr.SequenceNumber(0), txXDR1.Tx.SeqNum, "sequence number should be 0")
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
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")
}

func TestHashHex(t *testing.T) {
kp0 := newKeypair0()
sourceAccount := NewSimpleAccount(kp0.Address(), int64(9605939170639897))
Expand Down Expand Up @@ -957,4 +996,5 @@ func TestHashXTransaction(t *testing.T) {
assert.NoError(t, err)
expected = "AAAAANW8EOZG3RNV38krq5eSr1NNhco7DvfyBU/5mKERi7P0AAAAZAAPd2EAAAAHAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAITg3tq8G0kvnvoIhZPMYJsY+9KVV8xAA6NxhtKxIXZUAAAAAAAAAAAF9eEAAAAAAAAAAAGWLqMFAAAAQHRoaXMgaXMgYSBwcmVpbWFnZSBmb3IgaGFzaHggdHJhbnNhY3Rpb25zIG9uIHRoZSBzdGVsbGFyIG5ldHdvcms="
assert.Equal(t, expected, txeB64, "Base 64 XDR should match")

}