Class: Stellar::SEP10
Class Method Summary collapse
-
.build_challenge_tx(server:, client:, domain: nil, timeout: 300, **options) ⇒ String
Helper method to create a valid SEP0010 challenge transaction which you can use for Stellar Web Authentication.
-
.read_challenge_tx(server:, challenge_xdr:, **options) ⇒ Array(Stellar::TransactionEnvelope, String)
Reads a SEP 10 challenge transaction and returns the decoded transaction envelope and client account ID contained within.
-
.verify_challenge_tx_signers(server:, challenge_xdr:, signers:) ⇒ <String>
Verifies that for a SEP 10 challenge transaction all signatures on the transaction are accounted for.
-
.verify_challenge_tx_threshold(server:, challenge_xdr:, signers:, threshold:) ⇒ <String>
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.
-
.verify_tx_signatures(tx_envelope:, signers:) ⇒ Set<Stellar::KeyPair>
Verifies every signer passed matches a signature on the transaction exactly once, returning a list of unique signers that were found to have signed the transaction.
-
.verify_tx_signed_by(tx_envelope:, keypair:) ⇒ Boolean
Verifies if a Stellar::TransactionEnvelope was signed by the given Stellar::KeyPair.
Class Method Details
.build_challenge_tx(server:, client:, domain: nil, timeout: 300, **options) ⇒ String
Helper method to create a valid SEP0010 challenge transaction which you can use for Stellar Web Authentication.
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/stellar/sep10.rb', line 23 def self.build_challenge_tx(server:, client:, domain: nil, timeout: 300, **) if domain.blank? && .key?(:anchor_name) ActiveSupport::Deprecation.new("next release", "stellar-sdk").warn <<~MSG SEP-10 v2.0.0 requires usage of service home domain instead of anchor name in the challenge transaction. Please update your implementation to use `Stellar::SEP10.build_challenge_tx(..., home_domain: 'example.com')`. Using `anchor_name` parameter makes your service incompatible with SEP10-2.0 clients, support for this parameter is deprecated and will be removed in the next major release of stellar-base. MSG domain = [:anchor_name] end # The value must be 64 bytes long. It contains a 48 byte # cryptographic-quality random string encoded using base64 (for a total of # 64 bytes after encoding). value = SecureRandom.base64(48) now = Time.now.to_i time_bounds = Stellar::TimeBounds.new( min_time: now, max_time: now + timeout ) tx = Stellar::TransactionBuilder.new( source_account: server, sequence_number: 0, time_bounds: time_bounds ).add_operation( Stellar::Operation.manage_data( name: "#{domain} auth", value: value, source_account: client ) ).build tx.to_envelope(server).to_xdr(:base64) end |
.read_challenge_tx(server:, challenge_xdr:, **options) ⇒ Array(Stellar::TransactionEnvelope, String)
Reads a SEP 10 challenge transaction and returns the decoded transaction envelope 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 either verify_challenge_tx_threshold or verify_challenge_tx_signers to completely verify the signed challenge
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/stellar/sep10.rb', line 78 def self.read_challenge_tx(server:, challenge_xdr:, **) envelope = Stellar::TransactionEnvelope.from_xdr(challenge_xdr, "base64") transaction = envelope.tx if transaction.seq_num != 0 raise InvalidSep10ChallengeError, "The transaction sequence number should be zero" end if transaction.source_account != server.muxed_account raise InvalidSep10ChallengeError, "The transaction source account is not equal to the server's account" end if transaction.operations.size < 1 raise InvalidSep10ChallengeError, "The transaction should contain at least one operation" end auth_op, *rest_ops = transaction.operations client_account_id = auth_op.source_account if client_account_id.nil? raise InvalidSep10ChallengeError, "The transaction's operation should contain a source account" end if auth_op.body.arm != :manage_data_op raise InvalidSep10ChallengeError, "The transaction's first operation should be manageData" end if auth_op.body.value.data_value.unpack1("m").size != 48 raise InvalidSep10ChallengeError, "The transaction's operation value should be a 64 bytes base64 random string" end rest_ops.each do |op| if op.body.arm != :manage_data_op raise InvalidSep10ChallengeError, "The transaction has operations that are not of type 'manageData'" elsif op.source_account != server.muxed_account raise InvalidSep10ChallengeError, "The transaction has operations that are unrecognized" end end unless verify_tx_signed_by(tx_envelope: envelope, keypair: server) raise InvalidSep10ChallengeError, "The transaction is not signed by the server" end time_bounds = transaction.time_bounds now = Time.now.to_i if time_bounds.nil? || !now.between?(time_bounds.min_time, time_bounds.max_time) raise InvalidSep10ChallengeError, "The transaction has expired" end # Mirror the return type of the other SDK's and return a string client_kp = Stellar::KeyPair.from_public_key(client_account_id.ed25519!) [envelope, client_kp.address] end |
.verify_challenge_tx_signers(server:, challenge_xdr:, signers:) ⇒ <String>
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.
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/stellar/sep10.rb', line 180 def self.verify_challenge_tx_signers(server:, challenge_xdr:, signers:) raise InvalidSep10ChallengeError, "no signers provided" if signers.empty? te, _ = read_challenge_tx(server: server, challenge_xdr: challenge_xdr) # ignore non-G signers and server's own address client_signers = signers.select { |s| s =~ /G[A-Z0-9]{55}/ && s != server.address }.to_set raise InvalidSep10ChallengeError, "at least one regular signer must be provided" if client_signers.empty? # verify all signatures in one pass client_signers.add(server.address) signers_found = verify_tx_signatures(tx_envelope: te, signers: client_signers) # ensure server signed transaction and remove it unless signers_found.delete?(server.address) raise InvalidSep10ChallengeError, "Transaction not signed by server: #{server.address}" end # Confirm we matched signatures to the client signers. if signers_found.empty? raise InvalidSep10ChallengeError, "Transaction not signed by any client signer." end # Confirm all signatures were consumed by a signer. if signers_found.size != te.signatures.length - 1 raise InvalidSep10ChallengeError, "Transaction has unrecognized signatures." end signers_found end |
.verify_challenge_tx_threshold(server:, challenge_xdr:, signers:, threshold:) ⇒ <String>
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.
150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/stellar/sep10.rb', line 150 def self.verify_challenge_tx_threshold(server:, challenge_xdr:, signers:, threshold:) signers_found = verify_challenge_tx_signers( server: server, challenge_xdr: challenge_xdr, signers: signers.keys ) total_weight = signers.values_at(*signers_found).sum if total_weight < threshold raise InvalidSep10ChallengeError, "signers with weight #{total_weight} do not meet threshold #{threshold}." end signers_found end |
.verify_tx_signatures(tx_envelope:, signers:) ⇒ Set<Stellar::KeyPair>
Verifies every signer passed matches a signature on the transaction exactly once, returning a list of unique signers that were found to have signed the transaction.
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/stellar/sep10.rb', line 218 def self.verify_tx_signatures(tx_envelope:, signers:) signatures = tx_envelope.signatures if signatures.empty? raise InvalidSep10ChallengeError, "Transaction has no signatures." end tx_hash = tx_envelope.tx.hash to_keypair = Stellar::DSL.method(:KeyPair) keys_by_hint = signers.map(&to_keypair).index_by(&:signature_hint) tx_envelope.signatures.each.with_object(Set.new) do |sig, result| key = keys_by_hint.delete(sig.hint) result.add(key.address) if key&.verify(sig.signature, tx_hash) end end |
.verify_tx_signed_by(tx_envelope:, keypair:) ⇒ Boolean
Verifies if a Stellar::TransactionEnvelope was signed by the given Stellar::KeyPair
243 244 245 246 247 248 249 250 |
# File 'lib/stellar/sep10.rb', line 243 def self.verify_tx_signed_by(tx_envelope:, keypair:) tx_hash = tx_envelope.tx.hash tx_envelope.signatures.any? do |sig| next if sig.hint != keypair.signature_hint keypair.verify(sig.signature, tx_hash) end end |