Skip to content

Commit 5087e59

Browse files
committed
refactor: SEP-0010, confirm all signatures were consumed by a signer. (#252)
Thanks to Leigh McCulloch (@leighmcculloch).
1 parent 1ed6b70 commit 5087e59

File tree

3 files changed

+71
-8
lines changed

3 files changed

+71
-8
lines changed

stellar_sdk/sep/ed25519_public_key_signer.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
class Ed25519PublicKeySigner:
2-
def __init__(self, account_id: str, weight: int) -> None:
2+
def __init__(self, account_id: str, weight: int = 0) -> None:
33
"""The :class:`Signer` object, which represents represents the signer for the client account.
44
55
:param account_id: Account ID (ex. GBYNR2QJXLBCBTRN44MRORCMI4YO7FZPFBCNOKTOBCAAFC7KC3LNPRYS)

stellar_sdk/sep/stellar_web_authentication.py

+32-7
Original file line numberDiff line numberDiff line change
@@ -191,19 +191,44 @@ def verify_challenge_transaction_signers(
191191
)
192192
server_keypair = Keypair.from_public_key(server_account_id)
193193

194-
# Remove the server signer from the signers list if it is present. It's
195-
# important when verifying signers of a challenge transaction that we only
196-
# verify and return client signers. If an account has the server as a
197-
# signer the server should not play a part in the authentication of the
198-
# client.
194+
# Deduplicate the client signers and ensure the server is not included
195+
# anywhere we check or output the list of signers.
199196
client_signers = []
200197
for signer in signers:
198+
# Ignore the server signer if it is in the signers list. It's
199+
# important when verifying signers of a challenge transaction that we
200+
# only verify and return client signers. If an account has the server
201+
# as a signer the server should not play a part in the authentication
202+
# of the client.
201203
if signer == server_keypair.public_key:
202204
continue
203205
client_signers.append(signer)
204206

205-
signers_found = _verify_transaction_signatures(te, client_signers)
206-
if len(signers_found) != len(te.signatures) - 1:
207+
# Verify all the transaction's signers (server and client) in one
208+
# hit. We do this in one hit here even though the server signature was
209+
# checked in the read_challenge_transaction to ensure that every signature and signer
210+
# are consumed only once on the transaction.
211+
all_signers = client_signers + [Ed25519PublicKeySigner(server_keypair.public_key)]
212+
all_signers_found = _verify_transaction_signatures(te, all_signers)
213+
214+
signers_found = []
215+
server_signer_found = False
216+
for signer in all_signers_found:
217+
if signer.account_id == server_keypair.public_key:
218+
server_signer_found = True
219+
continue
220+
if signer in signers_found:
221+
continue
222+
signers_found.append(signer)
223+
224+
# Confirm we matched a signature to the server signer.
225+
if not server_signer_found:
226+
raise InvalidSep10ChallengeError(
227+
"Transaction not signed by server: {}.".format(server_keypair.public_key)
228+
)
229+
230+
# Confirm all signatures were consumed by a signer.
231+
if len(all_signers_found) != len(te.signatures):
207232
raise InvalidSep10ChallengeError("Transaction has unrecognized signatures.")
208233

209234
return signers_found

tests/sep/test_stellar_web_authentication.py

+38
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,44 @@ def test_verify_challenge_transaction_signers_raise_no_signers(self):
509509
challenge_tx, server_kp.public_key, network_passphrase, signers
510510
)
511511

512+
def test_verify_challenge_transaction_signers_raise_no_server_signature(self):
513+
server_kp = Keypair.random()
514+
client_kp_a = Keypair.random()
515+
client_kp_b = Keypair.random()
516+
client_kp_c = Keypair.random()
517+
timeout = 600
518+
network_passphrase = Network.PUBLIC_NETWORK_PASSPHRASE
519+
anchor_name = "SDF"
520+
521+
challenge = build_challenge_transaction(
522+
server_secret=server_kp.secret,
523+
client_account_id=client_kp_a.public_key,
524+
anchor_name=anchor_name,
525+
network_passphrase=network_passphrase,
526+
timeout=timeout,
527+
)
528+
529+
transaction = TransactionEnvelope.from_xdr(challenge, network_passphrase)
530+
transaction.signatures = []
531+
transaction.sign(client_kp_a)
532+
transaction.sign(client_kp_b)
533+
transaction.sign(client_kp_c)
534+
535+
challenge_tx = transaction.to_xdr()
536+
signers = [
537+
Ed25519PublicKeySigner(client_kp_a.public_key, 1),
538+
Ed25519PublicKeySigner(client_kp_a.public_key, 3),
539+
Ed25519PublicKeySigner(client_kp_a.public_key, 4),
540+
Ed25519PublicKeySigner(Keypair.random().public_key, 255),
541+
]
542+
with pytest.raises(
543+
InvalidSep10ChallengeError,
544+
match="Transaction not signed by server: {}.".format(server_kp.public_key),
545+
):
546+
verify_challenge_transaction_signers(
547+
challenge_tx, server_kp.public_key, network_passphrase, signers
548+
)
549+
512550
def test_verify_challenge_transaction_signers_raise_unrecognized_signatures(self):
513551
server_kp = Keypair.random()
514552
client_kp_a = Keypair.random()

0 commit comments

Comments
 (0)