Class: Stellar::SEP10
Constant Summary collapse
- GRACE_PERIOD =
We use a small grace period for the challenge transaction time bounds to compensate possible clock drift on client’s machine
5.minutes
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.
- .extract_client_domain_account(transaction) ⇒ Object
-
.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.
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 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/stellar/sep10.rb', line 27 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 now = Time.now.to_i time_bounds = Stellar::TimeBounds.new( min_time: now, max_time: now + timeout ) tb = Stellar::TransactionBuilder.new( source_account: server, sequence_number: 0, time_bounds: time_bounds ) # 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). tb.add_operation( Stellar::Operation.manage_data( name: "#{domain} auth", value: SecureRandom.base64(48), source_account: client ) ) if .key?(:auth_domain) tb.add_operation( Stellar::Operation.manage_data( name: "web_auth_domain", value: [:auth_domain], source_account: server ) ) end if [:client_domain].present? if [:client_domain_account].blank? raise "`client_domain_account` is required, if `client_domain` is provided" end tb.add_operation( Stellar::Operation.manage_data( name: "client_domain", value: [:client_domain], source_account: [:client_domain_account] ) ) end tb.build.to_envelope(server).to_xdr(:base64) end |
.extract_client_domain_account(transaction) ⇒ Object
299 300 301 302 303 304 305 306 307 308 |
# File 'lib/stellar/sep10.rb', line 299 def self.extract_client_domain_account(transaction) client_domain_account_op = transaction .operations .find { |op| op.body.value.data_name == "client_domain" } return if client_domain_account_op.blank? Util::StrKey.encode_muxed_account(client_domain_account_op.source_account) 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
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 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/stellar/sep10.rb', line 107 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 auth_op_body = auth_op.body.value if client_account_id.blank? 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 .key?(:domain) && auth_op_body.data_name != "#{[:domain]} auth" raise InvalidSep10ChallengeError, "The transaction's operation data name is invalid" end if auth_op_body.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| body = op.body op_params = body.value if body.arm != :manage_data_op raise InvalidSep10ChallengeError, "The transaction has operations that are not of type 'manageData'" elsif op.source_account != server.muxed_account && op_params.data_name != "client_domain" raise InvalidSep10ChallengeError, "The transaction has operations that are unrecognized" elsif op_params.data_name == "web_auth_domain" && .key?(:auth_domain) && op_params.data_value != [:auth_domain] raise InvalidSep10ChallengeError, "The transaction has 'manageData' operation with 'web_auth_domain' key and invalid value" 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.cond.time_bounds now = Time.now.to_i if time_bounds.blank? || !now.between?(time_bounds.min_time - GRACE_PERIOD, time_bounds.max_time + GRACE_PERIOD) 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.
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
# File 'lib/stellar/sep10.rb', line 220 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? client_domain_account_address = extract_client_domain_account(te.tx) client_signers.add(client_domain_account_address) if client_domain_account_address.present? # 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 if client_domain_account_address.present? && !signers_found.include?(client_domain_account_address) raise InvalidSep10ChallengeError, "Transaction not signed by client domain account." 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.
190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/stellar/sep10.rb', line 190 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.
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 |
# File 'lib/stellar/sep10.rb', line 265 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) 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
290 291 292 293 294 295 296 297 |
# File 'lib/stellar/sep10.rb', line 290 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 |