Skip to content

txnbuild: update challenge tx helpers for SEP-10 v1.3.0 #2071

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

Merged
merged 34 commits into from
Jan 29, 2020
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7f8d793
txnbuild: update challenge tx helpers for sep-10 changes
leighmcculloch Dec 17, 2019
1bd551f
feedback from @ire-and-curses
leighmcculloch Dec 18, 2019
4fccf79
Merge branch 'master' into sep10prop
leighmcculloch Dec 20, 2019
ad17003
Add examples of using Horizon during verification
leighmcculloch Dec 21, 2019
c3a7169
Simplify the interface and make it easier to implement a check correctly
leighmcculloch Dec 21, 2019
828d6ec
Make example comments clearer
leighmcculloch Dec 21, 2019
3aacce3
Make the example more realistic by handling account not existing
leighmcculloch Dec 21, 2019
2432aed
Reduce dupe code
leighmcculloch Dec 21, 2019
b051ebc
Improve comments
leighmcculloch Dec 21, 2019
ab75f08
Merge branch 'master' into sep10prop
leighmcculloch Dec 21, 2019
3ca7923
Merge branch 'master' into sep10prop
leighmcculloch Jan 15, 2020
b419bad
add test
leighmcculloch Jan 15, 2020
56097e4
update comments
leighmcculloch Jan 15, 2020
fe76f9e
Add clarifying comment to deprecated func
leighmcculloch Jan 16, 2020
6492759
Add tests for VerifyChallengeTxThreshold
leighmcculloch Jan 16, 2020
846e396
Add check that some signers are provided
leighmcculloch Jan 16, 2020
a83ffec
Merge branch 'master' into sep10prop
leighmcculloch Jan 16, 2020
f20703f
Remove dependency on clients/horizon
leighmcculloch Jan 16, 2020
b7bec9f
Remove addition of protocols/horizon dependency
leighmcculloch Jan 21, 2020
731c0d4
Change how the signer data is copied from horizon to txnbuild
leighmcculloch Jan 22, 2020
d53c0b7
Fix the order of the signers that are outputted
leighmcculloch Jan 22, 2020
0a9dbc8
Moved SignerSummary to its own file
leighmcculloch Jan 22, 2020
99529e7
Fix bug in weight >= threshold check
leighmcculloch Jan 22, 2020
e2519cf
Fix tests failing due to slice ordering
leighmcculloch Jan 22, 2020
d3420d8
Fix test expectation
leighmcculloch Jan 22, 2020
2a9e88d
Merge branch 'master' into sep10prop
leighmcculloch Jan 24, 2020
330f692
Improve deprecated comment
leighmcculloch Jan 27, 2020
30a4ef9
Restructured some of the challenge transaction verification process t…
leighmcculloch Jan 28, 2020
3fedc4c
Remove unnecessary word in comment
leighmcculloch Jan 28, 2020
916a4cf
More consistent variable names
leighmcculloch Jan 28, 2020
fb8198b
Preserve the order of client signers throughout the whole process
leighmcculloch Jan 28, 2020
2a903e0
Add test proving duplicate signers in request are not included in errors
leighmcculloch Jan 28, 2020
0914d38
Add tests to all functions to ensure server key missing will fail, an…
leighmcculloch Jan 28, 2020
a7e3838
More meaningful error message if signer is a seed
leighmcculloch Jan 28, 2020
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
9 changes: 9 additions & 0 deletions protocols/horizon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions txnbuild/signer_summary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package txnbuild

// SignerSummary is a map of signers to their weights.
type SignerSummary map[string]int32
247 changes: 205 additions & 42 deletions txnbuild/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
"time"

"github.com/stellar/go/keypair"
Expand Down Expand Up @@ -413,77 +414,214 @@ 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) {
signerAddresses := make([]string, 0, len(signerSummary))
for s := range signerSummary {
signerAddresses = append(signerAddresses, s)
}

signersFound, err = VerifyChallengeTxSigners(challengeTx, serverAccountID, network, signerAddresses...)
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.
clientSignersSet := 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
}
clientSignersSet[signer] = struct{}{}
}
clientSigners := make([]string, 0, len(clientSignersSet))
for signer := range clientSignersSet {
clientSigners = append(clientSigners, signer)
}

// Verify that 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
}

// Remove the server from the list of signers found.
signersFound := make([]string, 0, len(allSignersFound)-1)
for _, signer := range allSignersFound {
if signer == serverKP.Address() {
continue
}
signersFound = append(signersFound, signer)
}

// Confirm we found at least one client signer.
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")
}

// verify signature from operation source
err = verifyTxSignature(tx, op.SourceAccount.GetAccountID())
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
}
Expand All @@ -492,33 +630,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, err
}

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
}
Loading