Class: Stellar::SEP10

Inherits:
Object
  • Object
show all
Includes:
DSL
Defined in:
lib/stellar/sep10.rb

Class Method Summary collapse

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.

Examples:

server = Stellar::KeyPair.random # SIGNING_KEY from your stellar.toml
user = Stellar::KeyPair.from_address('G...')
Stellar::SEP10.build_challenge_tx(server: server, client: user, domain: 'example.com', timeout: 300)

Parameters:

  • server (Stellar::KeyPair)

    server’s signing keypair (SIGNING_KEY in service’s stellar.toml)

  • client (Stellar::KeyPair)

    account trying to authenticate with the server

  • domain (String) (defaults to: nil)

    service’s domain to be used in the manage_data key

  • timeout (Integer) (defaults to: 300)

    challenge duration (default to 5 minutes)

Returns:

  • (String)

    A base64 encoded string of the raw TransactionEnvelope xdr struct for the transaction.

See Also:

  • Stellar Web Authentication}[https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md]


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, **options)
  if domain.blank? && options.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 = options[: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

Examples:

sep10 = Stellar::SEP10
server = Stellar::KeyPair.random # this should be the SIGNING_KEY from your stellar.toml
challenge = sep10.build_challenge_tx(server: server, client: user, home_domain: domain, timeout: timeout)
envelope, client_address = sep10.read_challenge_tx(server: server, challenge: challenge)

Parameters:

  • challenge_xdr (String)

    SEP0010 transaction challenge in base64.

  • server (Stellar::KeyPair)

    keypair for server where the challenge was generated.

Returns:

  • (Array(Stellar::TransactionEnvelope, String))


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:, **options)
  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. != server.
    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
   = auth_op.

  if .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. != server.
      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(.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.

Parameters:

  • server (Stellar::Keypair)

    server’s signing key

  • challenge_xdr (String)

    SEP0010 transaction challenge transaction in base64.

  • signers (<String>)

    The signers of client account.

Returns:

  • (<String>)

    subset of input signers who have signed ‘challenge_xdr`

Raises:

  • InvalidSep10ChallengeError one or more signatures in the transaction are not identifiable as the server account or one of the signers provided in the arguments



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.

Parameters:

  • server (Stellar::KeyPair)

    keypair for server’s account.

  • challenge_xdr (String)

    SEP0010 challenge transaction in base64.

  • signers ({String => Integer})

    The signers of client account.

  • threshold (Integer)

    The medThreshold on the client account.

Returns:

  • (<String>)

    subset of input signers who have signed ‘challenge_xdr`

Raises:

  • InvalidSep10ChallengeError if the transaction has unrecognized signatures (only server’s signing key and keypairs found in the ‘signing` argument are recognized) or total weight of the signers does not meet the `threshold`



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.

Parameters:

  • tx_envelope (Stellar::TransactionEnvelope)

    SEP0010 transaction challenge transaction envelope.

  • signers (<String>)

    The signers of client account.

Returns:

  • (Set<Stellar::KeyPair>)


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

Examples:

Stellar::SEP10.verify_tx_signed_by(tx_envelope: envelope, keypair: keypair)

Parameters:

  • tx_envelope (Stellar::TransactionEnvelope)
  • keypair (Stellar::KeyPair)

Returns:

  • (Boolean)


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