diff --git a/protocols/horizon/main.go b/protocols/horizon/main.go index b1c034051d..198f19930c 100644 --- a/protocols/horizon/main.go +++ b/protocols/horizon/main.go @@ -128,6 +128,15 @@ func (a *Account) GetData(key string) ([]byte, error) { return base64.StdEncoding.DecodeString(a.Data[key]) } +// SignerSummary returns a map of signer's keys to weights. +func (a *Account) SignerSummary() map[string]int32 { + m := map[string]int32{} + for _, s := range a.Signers { + m[s.Key] = s.Weight + } + return m +} + // AccountSigner is the account signer information. type AccountSigner struct { Links struct { diff --git a/txnbuild/signer_summary.go b/txnbuild/signer_summary.go new file mode 100644 index 0000000000..269e65a8f7 --- /dev/null +++ b/txnbuild/signer_summary.go @@ -0,0 +1,4 @@ +package txnbuild + +// SignerSummary is a map of signers to their weights. +type SignerSummary map[string]int32 diff --git a/txnbuild/transaction.go b/txnbuild/transaction.go index 1210e73158..aae64dc11c 100644 --- a/txnbuild/transaction.go +++ b/txnbuild/transaction.go @@ -16,6 +16,7 @@ import ( "encoding/base64" "encoding/hex" "fmt" + "strings" "time" "github.com/stellar/go/keypair" @@ -413,77 +414,222 @@ 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) +// ReadChallengeTx reads a SEP 10 challenge transaction and returns the decoded +// transaction and client account ID contained within. +// +// It also verifies that transaction is signed by the server. +// +// It does not verify that the transaction has been signed by the client or +// that any signatures other than the servers on the transaction are valid. Use +// one of the following functions to completely verify the transaction: +// - VerifyChallengeTxThreshold +// - VerifyChallengeTxSigners +func ReadChallengeTx(challengeTx, serverAccountID, network string) (tx Transaction, clientAccountID string, err error) { + tx, err = TransactionFromXDR(challengeTx) if err != nil { - return false, err + return tx, clientAccountID, err } tx.Network = network // verify transaction source if tx.SourceAccount == nil { - return false, errors.New("transaction requires a source account") + return tx, clientAccountID, 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") + return tx, clientAccountID, errors.New("transaction source account is not equal to server's account") } - //verify sequence number + // verify sequence number txSourceAccount, ok := tx.SourceAccount.(*SimpleAccount) if !ok { - return false, errors.New("source account is not of type SimpleAccount unable to verify sequence number") + return tx, clientAccountID, errors.New("source account is not of type SimpleAccount unable to verify sequence number") } if txSourceAccount.Sequence != 0 { - return false, errors.New("transaction sequence number must be 0") + return tx, clientAccountID, errors.New("transaction sequence number must be 0") } // verify timebounds if tx.Timebounds.MaxTime == TimeoutInfinite { - return false, errors.New("transaction requires non-infinite timebounds") + return tx, clientAccountID, errors.New("transaction requires non-infinite timebounds") } currentTime := time.Now().UTC().Unix() if currentTime < tx.Timebounds.MinTime || currentTime > tx.Timebounds.MaxTime { - return false, errors.Errorf("transaction is not within range of the specified timebounds (currentTime=%d, MinTime=%d, MaxTime=%d)", + return tx, clientAccountID, errors.Errorf("transaction is not within range of the specified timebounds (currentTime=%d, MinTime=%d, MaxTime=%d)", currentTime, tx.Timebounds.MinTime, tx.Timebounds.MaxTime) } // verify operation if len(tx.Operations) != 1 { - return false, errors.New("transaction requires a single manage_data operation") + return tx, clientAccountID, 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") + return tx, clientAccountID, errors.New("operation type should be manage_data") } if op.SourceAccount == nil { - return false, errors.New("operation should have a source account") + return tx, clientAccountID, errors.New("operation should have a source account") } + clientAccountID = op.SourceAccount.GetAccountID() // verify manage data value nonceB64 := string(op.Value) if len(nonceB64) != 64 { - return false, errors.New("random nonce encoded as base64 should be 64 bytes long") + return tx, clientAccountID, errors.New("random nonce encoded as base64 should be 64 bytes long") } nonceBytes, err := base64.StdEncoding.DecodeString(nonceB64) if err != nil { - return false, errors.Wrap(err, "failed to decode random nonce provided in manage_data operation") + return tx, clientAccountID, errors.Wrap(err, "failed to decode random nonce provided in manage_data operation") } if len(nonceBytes) != 48 { - return false, errors.New("random nonce before encoding as base64 should be 48 bytes long") + return tx, clientAccountID, errors.New("random nonce before encoding as base64 should be 48 bytes long") + } + + err = verifyTxSignature(tx, serverAccountID) + if err != nil { + return tx, clientAccountID, err + } + + return tx, clientAccountID, nil +} + +// VerifyChallengeTxThreshold verifies that for a SEP 10 challenge transaction +// all signatures on the transaction are accounted for and that the signatures +// meet a threshold on an account. A transaction is verified if it is signed by +// the server account, and all other signatures match a signer that has been +// provided as an argument, and those signatures meet a threshold on the +// account. +// +// Errors will be raised if: +// - The transaction is invalid according to ReadChallengeTx. +// - No client signatures are found on the transaction. +// - One or more signatures in the transaction are not identifiable as the +// server account or one of the signers provided in the arguments. +// - The signatures are all valid but do not meet the threshold. +func VerifyChallengeTxThreshold(challengeTx, serverAccountID, network string, threshold Threshold, signerSummary SignerSummary) (signersFound []string, err error) { + signers := make([]string, 0, len(signerSummary)) + for s := range signerSummary { + signers = append(signers, s) + } + + signersFound, err = VerifyChallengeTxSigners(challengeTx, serverAccountID, network, signers...) + if err != nil { + return nil, err + } + + weight := int32(0) + for _, s := range signersFound { + weight += signerSummary[s] + } + + if weight < int32(threshold) { + return nil, errors.Errorf("signers with weight %d do not meet threshold %d", weight, threshold) + } + + return signersFound, nil +} + +// VerifyChallengeTxSigners verifies that for a SEP 10 challenge transaction +// all signatures on the transaction are accounted for. A transaction is +// verified if it is signed by the server account, and all other signatures +// match a signer that has been provided as an argument. Additional signers can +// be provided that do not have a signature, but all signatures must be matched +// to a signer for verification to succeed. If verification succeeds a list of +// signers that were found is returned, excluding the server account ID. +// +// Errors will be raised if: +// - The transaction is invalid according to ReadChallengeTx. +// - No client signatures are found on the transaction. +// - One or more signatures in the transaction are not identifiable as the +// server account or one of the signers provided in the arguments. +func VerifyChallengeTxSigners(challengeTx, serverAccountID, network string, signers ...string) ([]string, error) { + if len(signers) == 0 { + return nil, errors.New("no signers provided") + } + + // Read the transaction which validates its structure. + tx, _, err := ReadChallengeTx(challengeTx, serverAccountID, network) + if err != nil { + return nil, err + } + + // Ensure the server account ID is an address and not a seed. + serverKP, err := keypair.ParseAddress(serverAccountID) + if err != nil { + return nil, err + } + + // Deduplicate the client signers and ensure the server is not included + // anywhere we check or output the list of signers. + clientSigners := []string{} + clientSignersSeen := map[string]struct{}{} + for _, signer := range signers { + // Ignore the server signer if it is in the signers list. It's + // important when verifying signers of a challenge transaction that we + // only verify and return client signers. If an account has the server + // as a signer the server should not play a part in the authentication + // of the client. + if signer == serverKP.Address() { + continue + } + if _, seen := clientSignersSeen[signer]; seen { + continue + } + clientSigners = append(clientSigners, signer) + clientSignersSeen[signer] = struct{}{} + } + + // Verify all the transaction's signers (server and client) in one + // hit. We do this in one hit here even though the server signature was + // checked in the ReadChallengeTx to ensure that every signature and signer + // are consumed only once on the transaction. + allSigners := append([]string{serverKP.Address()}, clientSigners...) + allSignersFound, err := verifyTxSignatures(tx, allSigners...) + if err != nil { + return nil, err } - // verify signature from operation source - err = verifyTxSignature(tx, op.SourceAccount.GetAccountID()) + // Confirm the server is in the list of signers found and remove it. + serverSignerFound := false + signersFound := make([]string, 0, len(allSignersFound)-1) + for _, signer := range allSignersFound { + if signer == serverKP.Address() { + serverSignerFound = true + continue + } + signersFound = append(signersFound, signer) + } + + // Confirm we matched a signature to the server signer. + if !serverSignerFound { + return nil, errors.Errorf("transaction not signed by %s", serverKP.Address()) + } + + // Confirm we matched signatures to the client signers. + if len(signersFound) == 0 { + return nil, errors.Errorf("transaction not signed by %s", strings.Join(clientSigners, ", ")) + } + + // Confirm all signatures were consumed by a signer. + if len(allSignersFound) != len(tx.xdrEnvelope.Signatures) { + return signersFound, errors.Errorf("transaction has unrecognized signatures") + } + + return signersFound, nil +} + +// 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 account's master key. +// More details on SEP 10: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md +// +// Deprecated: Use VerifyChallengeTxThreshold or VerifyChallengeTxSigners. +func VerifyChallengeTx(challengeTx, serverAccountID, network string) (bool, error) { + tx, clientAccountID, err := ReadChallengeTx(challengeTx, serverAccountID, network) if err != nil { return false, err } - // verify signature from server signing key - err = verifyTxSignature(tx, serverAccountID) + err = verifyTxSignature(tx, clientAccountID) if err != nil { return false, err } @@ -492,33 +638,58 @@ func VerifyChallengeTx(challengeTx, serverAccountID, network string) (bool, erro } // verifyTxSignature checks if a transaction has been signed by the provided Stellar account. -func verifyTxSignature(tx Transaction, accountID string) error { - if tx.xdrEnvelope == nil { - return errors.New("transaction has no signatures") +func verifyTxSignature(tx Transaction, signer string) error { + signersFound, err := verifyTxSignatures(tx, signer) + if len(signersFound) == 0 { + return errors.Errorf("transaction not signed by %s", signer) } + return err +} - txHash, err := tx.Hash() - if err != nil { - return err +// verifyTxSignature checks if a transaction has been signed by one or more of +// the signers, returning a list of signers that were found to have signed the +// transaction. +func verifyTxSignatures(tx Transaction, signers ...string) ([]string, error) { + if tx.xdrEnvelope == nil { + return nil, errors.New("transaction has no signatures") } - kp, err := keypair.Parse(accountID) + txHash, err := tx.Hash() if err != nil { - return err + return nil, err } // find and verify signatures - signerFound := false - for _, s := range tx.xdrEnvelope.Signatures { - e := kp.Verify(txHash[:], s.Signature) - if e == nil { - signerFound = true - break + signatureUsed := map[int]bool{} + signersFound := map[string]struct{}{} + for _, signer := range signers { + kp, err := keypair.ParseAddress(signer) + if err != nil { + return nil, errors.Wrap(err, "signer not address") + } + + for i, decSig := range tx.xdrEnvelope.Signatures { + if signatureUsed[i] { + continue + } + if decSig.Hint != kp.Hint() { + continue + } + err := kp.Verify(txHash[:], decSig.Signature) + if err == nil { + signatureUsed[i] = true + signersFound[signer] = struct{}{} + break + } } - } - if !signerFound { - return errors.Errorf("transaction not signed by %s", accountID) } - return nil + signersFoundList := make([]string, 0, len(signersFound)) + for _, signer := range signers { + if _, ok := signersFound[signer]; ok { + signersFoundList = append(signersFoundList, signer) + delete(signersFound, signer) + } + } + return signersFoundList, nil } diff --git a/txnbuild/transaction_challenge_example_test.go b/txnbuild/transaction_challenge_example_test.go new file mode 100644 index 0000000000..8358cfc5ec --- /dev/null +++ b/txnbuild/transaction_challenge_example_test.go @@ -0,0 +1,130 @@ +package txnbuild_test + +import ( + "fmt" + "sort" + "time" + + "github.com/stellar/go/clients/horizon" + "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/keypair" + "github.com/stellar/go/network" + "github.com/stellar/go/txnbuild" +) + +var serverAccount, _ = keypair.ParseFull("SCDXPYDGKV5HOAGVZN3FQSS5FKUPP5BAVBWH4FXKTAWAC24AE4757JSI") +var clientAccount, _ = keypair.ParseFull("SANVNCABRBVISCV7KH4SZVBKPJWWTT4424OVWUHUHPH2MVSF6RC7HPGN") +var clientSigner1, _ = keypair.ParseFull("SBPQUZ6G4FZNWFHKUWC5BEYWF6R52E3SEP7R3GWYSM2XTKGF5LNTWW4R") +var clientSigner2, _ = keypair.ParseFull("SBMSVD4KKELKGZXHBUQTIROWUAPQASDX7KEJITARP4VMZ6KLUHOGPTYW") +var horizonClient = func() horizonclient.ClientInterface { + client := &horizonclient.MockClient{} + client. + On("AccountDetail", horizonclient.AccountRequest{AccountID: clientAccount.Address()}). + Return( + horizon.Account{ + Thresholds: horizon.AccountThresholds{LowThreshold: 1, MedThreshold: 10, HighThreshold: 100}, + Signers: []horizon.Signer{ + {Key: clientSigner1.Address(), Weight: 40}, + {Key: clientSigner2.Address(), Weight: 60}, + }, + }, + nil, + ) + return client +}() + +func ExampleVerifyChallengeTxThreshold() { + // Server builds challenge transaction + var challengeTx string + { + tx, err := txnbuild.BuildChallengeTx(serverAccount.Seed(), clientAccount.Address(), "test", network.TestNetworkPassphrase, time.Minute) + if err != nil { + fmt.Println("Error:", err) + return + } + challengeTx = tx + } + + // Client reads and signs challenge transaction + var signedChallengeTx string + { + tx, txClientAccountID, err := txnbuild.ReadChallengeTx(challengeTx, serverAccount.Address(), network.TestNetworkPassphrase) + if err != nil { + fmt.Println("Error:", err) + return + } + if txClientAccountID != clientAccount.Address() { + fmt.Println("Error: challenge tx is not for expected client account") + return + } + err = tx.Sign(clientSigner1, clientSigner2) + if err != nil { + fmt.Println("Error:", err) + return + } + signedChallengeTx, err = tx.Base64() + if err != nil { + fmt.Println("Error:", err) + return + } + } + + // Server verifies signed challenge transaction + { + _, txClientAccountID, err := txnbuild.ReadChallengeTx(challengeTx, serverAccount.Address(), network.TestNetworkPassphrase) + if err != nil { + fmt.Println("Error:", err) + return + } + + // Server gets account + clientAccountExists := false + horizonClientAccount, err := horizonClient.AccountDetail(horizonclient.AccountRequest{AccountID: txClientAccountID}) + if err == nil { + clientAccountExists = true + } else { + if hErr, ok := err.(*horizonclient.Error); ok && hErr.Problem.Type == "https://stellar.org/horizon-errors/not_found" { + fmt.Println("Account does not exist, use master key to verify") + } else { + fmt.Println("Error:", err) + return + } + } + + if clientAccountExists { + // Server gets list of signers from account + signerSummary := horizonClientAccount.SignerSummary() + + // Server chooses the threshold to require: low, med or high + threshold := txnbuild.Threshold(horizonClientAccount.Thresholds.MedThreshold) + + // Server verifies threshold is met + signers, err := txnbuild.VerifyChallengeTxThreshold(signedChallengeTx, serverAccount.Address(), network.TestNetworkPassphrase, threshold, signerSummary) + if err != nil { + fmt.Println("Error:", err) + return + } + fmt.Println("Client Signers Verified:") + sort.Strings(signers) + for _, signer := range signers { + fmt.Println(signer, "weight:", signerSummary[signer]) + } + } else { + // Server verifies that master key has signed challenge transaction + signersFound, err := txnbuild.VerifyChallengeTxSigners(signedChallengeTx, serverAccount.Address(), network.TestNetworkPassphrase, txClientAccountID) + if err != nil { + fmt.Println("Error:", err) + return + } + fmt.Println("Client Master Key Verified:") + for _, signerFound := range signersFound { + fmt.Println(signerFound) + } + } + } + + // Output: + // Client Signers Verified: + // GAS4V4O2B7DW5T7IQRPEEVCRXMDZESKISR7DVIGKZQYYV3OSQ5SH5LVP weight: 60 + // GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3 weight: 40 +} diff --git a/txnbuild/transaction_test.go b/txnbuild/transaction_test.go index 7a4f7fa296..5cf0178e03 100644 --- a/txnbuild/transaction_test.go +++ b/txnbuild/transaction_test.go @@ -3,9 +3,11 @@ package txnbuild import ( "crypto/sha256" "encoding/base64" + "strings" "testing" "time" + "github.com/stellar/go/keypair" "github.com/stellar/go/network" "github.com/stellar/go/strkey" "github.com/stellar/go/xdr" @@ -1163,6 +1165,1092 @@ func TestSignWithSecretKey(t *testing.T) { assert.Equal(t, expected, actual, "base64 xdr should match") } +func TestReadChallengeTx_validSignedByServerAndClient(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + readTx, readClientAccountID, err := ReadChallengeTx(tx64, serverKP.Address(), network.TestNetworkPassphrase) + assert.Equal(t, tx, readTx) + assert.Equal(t, clientKP.Address(), readClientAccountID) + assert.NoError(t, err) +} + +func TestReadChallengeTx_validSignedByServer(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + readTx, readClientAccountID, err := ReadChallengeTx(tx64, serverKP.Address(), network.TestNetworkPassphrase) + assert.Equal(t, tx, readTx) + assert.Equal(t, clientKP.Address(), readClientAccountID) + assert.NoError(t, err) +} + +func TestReadChallengeTx_invalidNotSignedByServer(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + readTx, readClientAccountID, err := ReadChallengeTx(tx64, serverKP.Address(), network.TestNetworkPassphrase) + assert.Equal(t, tx, readTx) + assert.Equal(t, clientKP.Address(), readClientAccountID) + assert.EqualError(t, err, "transaction not signed by "+serverKP.Address()) +} + +func TestReadChallengeTx_invalidCorrupted(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + tx64 = strings.ReplaceAll(tx64, "A", "B") + readTx, readClientAccountID, err := ReadChallengeTx(tx64, serverKP.Address(), network.TestNetworkPassphrase) + assert.Equal(t, Transaction{}, readTx) + assert.Equal(t, "", readClientAccountID) + assert.EqualError(t, err, "unable to unmarshal transaction envelope: xdr:decode: switch '68174084' is not valid enum value for union") +} + +func TestReadChallengeTx_invalidServerAccountIDMismatch(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(newKeypair2().Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + readTx, readClientAccountID, err := ReadChallengeTx(tx64, serverKP.Address(), network.TestNetworkPassphrase) + assert.Equal(t, tx, readTx) + assert.Equal(t, "", readClientAccountID) + assert.EqualError(t, err, "transaction source account is not equal to server's account") +} + +func TestReadChallengeTx_invalidSeqNoNotZero(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), 1234) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + readTx, readClientAccountID, err := ReadChallengeTx(tx64, serverKP.Address(), network.TestNetworkPassphrase) + assert.Equal(t, tx, readTx) + assert.Equal(t, "", readClientAccountID) + assert.EqualError(t, err, "transaction sequence number must be 0") +} + +func TestReadChallengeTx_invalidTimeboundsInfinite(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewInfiniteTimeout(), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + readTx, readClientAccountID, err := ReadChallengeTx(tx64, serverKP.Address(), network.TestNetworkPassphrase) + assert.Equal(t, tx, readTx) + assert.Equal(t, "", readClientAccountID) + assert.EqualError(t, err, "transaction requires non-infinite timebounds") +} + +func TestReadChallengeTx_invalidTimeboundsOutsideRange(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimebounds(0, 100), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + readTx, readClientAccountID, err := ReadChallengeTx(tx64, serverKP.Address(), network.TestNetworkPassphrase) + assert.Equal(t, tx, readTx) + assert.Equal(t, "", readClientAccountID) + assert.Error(t, err) + assert.Regexp(t, "transaction is not within range of the specified timebounds", err.Error()) +} + +func TestReadChallengeTx_invalidTooManyOperations(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op, &op}, + Timebounds: NewTimeout(300), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + readTx, readClientAccountID, err := ReadChallengeTx(tx64, serverKP.Address(), network.TestNetworkPassphrase) + assert.Equal(t, tx, readTx) + assert.Equal(t, "", readClientAccountID) + assert.EqualError(t, err, "transaction requires a single manage_data operation") +} + +func TestReadChallengeTx_invalidOperationWrongType(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := BumpSequence{ + SourceAccount: &opSource, + BumpTo: 0, + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(300), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + readTx, readClientAccountID, err := ReadChallengeTx(tx64, serverKP.Address(), network.TestNetworkPassphrase) + assert.Equal(t, tx, readTx) + assert.Equal(t, "", readClientAccountID) + assert.EqualError(t, err, "operation type should be manage_data") +} + +func TestReadChallengeTx_invalidOperationNoSourceAccount(t *testing.T) { + serverKP := newKeypair0() + txSource := NewSimpleAccount(serverKP.Address(), -1) + op := ManageData{ + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(300), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + readTx, readClientAccountID, err := ReadChallengeTx(tx64, serverKP.Address(), network.TestNetworkPassphrase) + assert.Equal(t, tx, readTx) + assert.Equal(t, "", readClientAccountID) + assert.EqualError(t, err, "operation should have a source account") +} + +func TestReadChallengeTx_invalidDataValueWrongEncodedLength(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 45))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(300), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + readTx, readClientAccountID, err := ReadChallengeTx(tx64, serverKP.Address(), network.TestNetworkPassphrase) + assert.Equal(t, tx, readTx) + assert.Equal(t, clientKP.Address(), readClientAccountID) + assert.EqualError(t, err, "random nonce encoded as base64 should be 64 bytes long") +} + +func TestReadChallengeTx_invalidDataValueCorruptBase64(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA?AAAAAAAAAAAAAAAAAAAAAAAAAA"), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(300), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + readTx, readClientAccountID, err := ReadChallengeTx(tx64, serverKP.Address(), network.TestNetworkPassphrase) + assert.Equal(t, tx, readTx) + assert.Equal(t, clientKP.Address(), readClientAccountID) + assert.EqualError(t, err, "failed to decode random nonce provided in manage_data operation: illegal base64 data at input byte 37") +} + +func TestReadChallengeTx_invalidDataValueWrongByteLength(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 47))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(300), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + readTx, readClientAccountID, err := ReadChallengeTx(tx64, serverKP.Address(), network.TestNetworkPassphrase) + assert.Equal(t, tx, readTx) + assert.Equal(t, clientKP.Address(), readClientAccountID) + assert.EqualError(t, err, "random nonce before encoding as base64 should be 48 bytes long") +} + +func TestVerifyChallengeTxThreshold_invalidServer(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + threshold := Threshold(1) + signerSummary := SignerSummary{ + clientKP.Address(): 1, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(clientKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxThreshold(tx64, serverKP.Address(), network.TestNetworkPassphrase, threshold, signerSummary) + assert.Empty(t, signersFound) + assert.EqualError(t, err, "transaction not signed by "+serverKP.Address()) +} + +func TestVerifyChallengeTxThreshold_validServerAndClientKeyMeetingThreshold(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + threshold := Threshold(1) + signerSummary := SignerSummary{ + clientKP.Address(): 1, + } + wantSigners := []string{ + clientKP.Address(), + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxThreshold(tx64, serverKP.Address(), network.TestNetworkPassphrase, threshold, signerSummary) + assert.ElementsMatch(t, wantSigners, signersFound) + assert.NoError(t, err) +} + +func TestVerifyChallengeTxThreshold_validServerAndMultipleClientKeyMeetingThreshold(t *testing.T) { + serverKP := newKeypair0() + clientKP1 := newKeypair1() + clientKP2 := newKeypair2() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP1.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + threshold := Threshold(3) + signerSummary := map[string]int32{ + clientKP1.Address(): 1, + clientKP2.Address(): 2, + } + wantSigners := []string{ + clientKP1.Address(), + clientKP2.Address(), + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP1, clientKP2) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxThreshold(tx64, serverKP.Address(), network.TestNetworkPassphrase, threshold, signerSummary) + assert.ElementsMatch(t, wantSigners, signersFound) + assert.NoError(t, err) +} + +func TestVerifyChallengeTxThreshold_validServerAndMultipleClientKeyMeetingThresholdSomeUnused(t *testing.T) { + serverKP := newKeypair0() + clientKP1 := newKeypair1() + clientKP2 := newKeypair2() + clientKP3 := keypair.MustRandom() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP1.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + threshold := Threshold(3) + signerSummary := SignerSummary{ + clientKP1.Address(): 1, + clientKP2.Address(): 2, + clientKP3.Address(): 2, + } + wantSigners := []string{ + clientKP1.Address(), + clientKP2.Address(), + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP1, clientKP2) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxThreshold(tx64, serverKP.Address(), network.TestNetworkPassphrase, threshold, signerSummary) + assert.ElementsMatch(t, wantSigners, signersFound) + assert.NoError(t, err) +} + +func TestVerifyChallengeTxThreshold_invalidServerAndMultipleClientKeyNotMeetingThreshold(t *testing.T) { + serverKP := newKeypair0() + clientKP1 := newKeypair1() + clientKP2 := newKeypair2() + clientKP3 := keypair.MustRandom() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP1.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + threshold := Threshold(10) + signerSummary := SignerSummary{ + clientKP1.Address(): 1, + clientKP2.Address(): 2, + clientKP3.Address(): 2, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP1, clientKP2) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + _, err = VerifyChallengeTxThreshold(tx64, serverKP.Address(), network.TestNetworkPassphrase, threshold, signerSummary) + assert.EqualError(t, err, "signers with weight 3 do not meet threshold 10") +} + +func TestVerifyChallengeTxThreshold_invalidClientKeyUnrecognized(t *testing.T) { + serverKP := newKeypair0() + clientKP1 := newKeypair1() + clientKP2 := newKeypair2() + clientKP3 := keypair.MustRandom() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP1.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + threshold := Threshold(10) + signerSummary := map[string]int32{ + clientKP1.Address(): 1, + clientKP2.Address(): 2, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP1, clientKP2, clientKP3) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + _, err = VerifyChallengeTxThreshold(tx64, serverKP.Address(), network.TestNetworkPassphrase, threshold, signerSummary) + assert.EqualError(t, err, "transaction has unrecognized signatures") +} + +func TestVerifyChallengeTxThreshold_invalidNoSigners(t *testing.T) { + serverKP := newKeypair0() + clientKP1 := newKeypair1() + clientKP2 := newKeypair2() + clientKP3 := keypair.MustRandom() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP1.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + threshold := Threshold(10) + signerSummary := SignerSummary{} + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP1, clientKP2, clientKP3) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + _, err = VerifyChallengeTxThreshold(tx64, serverKP.Address(), network.TestNetworkPassphrase, threshold, signerSummary) + assert.EqualError(t, err, "no signers provided") +} + +func TestVerifyChallengeTxThreshold_weightsAddToMoreThan8Bits(t *testing.T) { + serverKP := newKeypair0() + clientKP1 := newKeypair1() + clientKP2 := newKeypair2() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP1.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + threshold := Threshold(1) + signerSummary := SignerSummary{ + clientKP1.Address(): 255, + clientKP2.Address(): 1, + } + wantSigners := []string{ + clientKP1.Address(), + clientKP2.Address(), + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP1, clientKP2) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxThreshold(tx64, serverKP.Address(), network.TestNetworkPassphrase, threshold, signerSummary) + assert.ElementsMatch(t, wantSigners, signersFound) + assert.NoError(t, err) +} + +func TestVerifyChallengeTxSigners_invalidServer(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(clientKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxSigners(tx64, serverKP.Address(), network.TestNetworkPassphrase, clientKP.Address()) + assert.Empty(t, signersFound) + assert.EqualError(t, err, "transaction not signed by "+serverKP.Address()) +} + +func TestVerifyChallengeTxSigners_validServerAndClientMasterKey(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxSigners(tx64, serverKP.Address(), network.TestNetworkPassphrase, clientKP.Address()) + assert.Equal(t, []string{clientKP.Address()}, signersFound) + assert.NoError(t, err) +} + +func TestVerifyChallengeTxSigners_invalidServerAndNoClient(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxSigners(tx64, serverKP.Address(), network.TestNetworkPassphrase, clientKP.Address()) + assert.Empty(t, signersFound) + assert.EqualError(t, err, "transaction not signed by "+clientKP.Address()) +} + +func TestVerifyChallengeTxSigners_invalidServerAndUnrecognizedClient(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + unrecognizedKP := newKeypair2() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, unrecognizedKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxSigners(tx64, serverKP.Address(), network.TestNetworkPassphrase, clientKP.Address()) + assert.Empty(t, signersFound) + assert.EqualError(t, err, "transaction not signed by "+clientKP.Address()) +} + +func TestVerifyChallengeTxSigners_validServerAndMultipleClientSigners(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + clientKP2 := newKeypair2() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP, clientKP2) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxSigners(tx64, serverKP.Address(), network.TestNetworkPassphrase, clientKP.Address(), clientKP2.Address()) + assert.Equal(t, []string{clientKP.Address(), clientKP2.Address()}, signersFound) + assert.NoError(t, err) +} + +func TestVerifyChallengeTxSigners_validServerAndMultipleClientSignersReverseOrder(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + clientKP2 := newKeypair2() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP2, clientKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxSigners(tx64, serverKP.Address(), network.TestNetworkPassphrase, clientKP.Address(), clientKP2.Address()) + assert.Equal(t, []string{clientKP.Address(), clientKP2.Address()}, signersFound) + assert.NoError(t, err) +} + +func TestVerifyChallengeTxSigners_validServerAndClientSignersNotMasterKey(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + clientKP2 := newKeypair2() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP2) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxSigners(tx64, serverKP.Address(), network.TestNetworkPassphrase, clientKP2.Address()) + assert.Equal(t, []string{clientKP2.Address()}, signersFound) + assert.NoError(t, err) +} + +func TestVerifyChallengeTxSigners_validServerAndClientSignersIgnoresServerSigner(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + clientKP2 := newKeypair2() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP2) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxSigners(tx64, serverKP.Address(), network.TestNetworkPassphrase, serverKP.Address(), clientKP2.Address()) + assert.Equal(t, []string{clientKP2.Address()}, signersFound) + assert.NoError(t, err) +} + +func TestVerifyChallengeTxSigners_invalidServerNoClientSignersIgnoresServerSigner(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + clientKP2 := newKeypair2() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxSigners(tx64, serverKP.Address(), network.TestNetworkPassphrase, serverKP.Address(), clientKP2.Address()) + assert.Empty(t, signersFound) + assert.EqualError(t, err, "transaction not signed by "+clientKP2.Address()) +} + +func TestVerifyChallengeTxSigners_validServerAndClientSignersIgnoresDuplicateSigner(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + clientKP2 := newKeypair2() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP2) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxSigners(tx64, serverKP.Address(), network.TestNetworkPassphrase, clientKP2.Address(), clientKP2.Address()) + assert.Equal(t, []string{clientKP2.Address()}, signersFound) + assert.NoError(t, err) +} + +func TestVerifyChallengeTxSigners_invalidServerAndClientSignersIgnoresDuplicateSignerInError(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + clientKP2 := newKeypair2() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP2) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxSigners(tx64, serverKP.Address(), network.TestNetworkPassphrase, clientKP.Address(), clientKP.Address()) + assert.Empty(t, signersFound) + assert.EqualError(t, err, "transaction not signed by "+clientKP.Address()) +} + +func TestVerifyChallengeTxSigners_invalidServerAndClientSignersFailsDuplicateSignatures(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + clientKP2 := newKeypair2() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP2, clientKP2) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxSigners(tx64, serverKP.Address(), network.TestNetworkPassphrase, clientKP2.Address()) + assert.Equal(t, []string{clientKP2.Address()}, signersFound) + assert.EqualError(t, err, "transaction has unrecognized signatures") +} + +func TestVerifyChallengeTxSigners_invalidServerAndClientSignersFailsSignerSeed(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + clientKP2 := newKeypair2() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP2, clientKP2) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + signersFound, err := VerifyChallengeTxSigners(tx64, serverKP.Address(), network.TestNetworkPassphrase, clientKP2.Seed()) + assert.Empty(t, signersFound) + assert.EqualError(t, err, "signer not address: invalid version byte") +} + +func TestVerifyChallengeTxSigners_invalidNoSigners(t *testing.T) { + serverKP := newKeypair0() + clientKP := newKeypair1() + txSource := NewSimpleAccount(serverKP.Address(), -1) + opSource := NewSimpleAccount(clientKP.Address(), 0) + op := ManageData{ + SourceAccount: &opSource, + Name: "testserver auth", + Value: []byte(base64.StdEncoding.EncodeToString(make([]byte, 48))), + } + tx := Transaction{ + SourceAccount: &txSource, + Operations: []Operation{&op}, + Timebounds: NewTimeout(1000), + Network: network.TestNetworkPassphrase, + } + + err := tx.Build() + require.NoError(t, err) + err = tx.Sign(serverKP, clientKP) + assert.NoError(t, err) + tx64, err := tx.Base64() + require.NoError(t, err) + _, err = VerifyChallengeTxSigners(tx64, serverKP.Address(), network.TestNetworkPassphrase) + assert.EqualError(t, err, "no signers provided") +} + func TestVerifyTxSignatureUnsignedTx(t *testing.T) { kp0 := newKeypair0() kp1 := newKeypair1()