-
Notifications
You must be signed in to change notification settings - Fork 44
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
chore: refactor SEP-10 #286
base: main
Are you sure you want to change the base?
Changes from 4 commits
a0fa590
a14ec27
84cbc31
3a3d143
f00a6d6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
--format documentation | ||
--color | ||
--require spec_helper |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
## [Unreleased] | ||
|
||
## [0.1.0] - 2022-10-29 | ||
Check notice Code scanning / Remark-lint (reported by Codacy) Warn when references to undefined definitions are found. Note
[no-undefined-references] Found reference to undefined definition
|
||
|
||
- Initial release | ||
Check notice Code scanning / Remark-lint (reported by Codacy) Warn when the spacing between a list item’s bullet and its content violates Note
[list-item-indent] Incorrect list-item indent: add 2 spaces
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# Stellar::Ecosystem | ||
|
||
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/stellar/ecosystem`. To experiment with that code, run `bin/console` for an interactive prompt. | ||
|
||
TODO: Delete this and the text above, and describe your gem | ||
|
||
## Installation | ||
|
||
Add this line to your application's Gemfile: | ||
|
||
```ruby | ||
gem 'stellar-ecosystem' | ||
``` | ||
|
||
And then execute: | ||
|
||
$ bundle install | ||
Check notice Code scanning / Remark-lint (reported by Codacy) Warn when code blocks do not adhere to a given style. Note
[code-block-style] Code blocks should be fenced
|
||
|
||
Or install it yourself as: | ||
|
||
$ gem install stellar-ecosystem | ||
Check notice Code scanning / Remark-lint (reported by Codacy) Warn when code blocks do not adhere to a given style. Note
[code-block-style] Code blocks should be fenced
|
||
|
||
## Usage | ||
|
||
TODO: Write usage instructions here | ||
|
||
## Development | ||
|
||
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. | ||
|
||
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). | ||
|
||
## Contributing | ||
|
||
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/stellar-ecosystem. | ||
Check notice Code scanning / Remark-lint (reported by Codacy) Warn when references to undefined definitions are found. Note
[no-undefined-references] Found reference to undefined definition
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# frozen_string_literal: true | ||
|
||
require "bundler/gem_tasks" | ||
require "rspec/core/rake_task" | ||
|
||
RSpec::Core::RakeTask.new(:spec) | ||
|
||
require "standard/rake" | ||
|
||
task default: %i[spec standard] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
#!/usr/bin/env ruby | ||
# frozen_string_literal: true | ||
|
||
require "bundler/setup" | ||
require "stellar/ecosystem" | ||
|
||
# You can add fixtures and/or initialization code here to make experimenting | ||
# with your gem easier. You can also use a different console, if you like. | ||
|
||
# (If you use this, don't forget to add pry to your Gemfile!) | ||
# require "pry" | ||
# Pry.start | ||
|
||
require "irb" | ||
IRB.start(__FILE__) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
#!/usr/bin/env bash | ||
set -euo pipefail | ||
IFS=$'\n\t' | ||
Check warning Code scanning / Semgrep Semgrep Finding: bash.lang.security.ifs-tampering.ifs-tampering Warning
The special variable IFS affects how splitting takes place when expanding unquoted variables. Don't set it globally. Prefer a dedicated utility such as 'cut' or 'awk' if you need to split input data. If you must use 'read', set IFS locally using e.g. 'IFS="," read -a my_array'.
|
||
set -vx | ||
|
||
bundle install | ||
|
||
# Do any other automated setup that you need to do here |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# frozen_string_literal: true | ||
|
||
require "stellar-base" | ||
|
||
module Stellar | ||
module Ecosystem | ||
VERSION = ::Stellar::VERSION | ||
end | ||
end | ||
|
||
require_relative "./stellar/sep10/challenge" | ||
require_relative "./stellar/sep10/challenge_tx_builder" | ||
require_relative "./stellar/sep10/server" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# frozen_string_literal: true | ||
|
||
module Stellar | ||
module Ecosystem | ||
VERSION = "0.1.0" | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
module Stellar | ||
module Ecosystem | ||
module SEP10 | ||
class InvalidChallengeError < StandardError; end | ||
|
||
class Challenge | ||
# We use a small grace period for the challenge transaction time bounds | ||
# to compensate possible clock drift on client's machine | ||
GRACE_PERIOD = 5.minutes | ||
|
||
def self.build(server:, client:, domain: nil, timeout: 300, **options) | ||
tx = ChallengeTxBuilder.build( | ||
server: server, | ||
client: client, | ||
domain: domain, | ||
timeout: timeout, | ||
**options | ||
) | ||
|
||
new(envelope: tx.to_envelope(server), server: server) | ||
end | ||
|
||
def self.read_xdr(xdr, server:) | ||
envelope = Stellar::TransactionEnvelope.from_xdr(xdr, "base64") | ||
new(envelope: envelope, server: server) | ||
end | ||
|
||
def initialize(envelope:, server:) | ||
@envelope = envelope | ||
@tx = envelope.tx | ||
@server = server | ||
end | ||
|
||
def to_xdr | ||
@envelope.to_xdr(:base64) | ||
end | ||
|
||
def to_envelope | ||
@envelope.clone | ||
end | ||
|
||
def validate!(**options) | ||
validate_tx! | ||
validate_operations!(options) | ||
|
||
raise InvalidChallengeError, "The transaction is not signed by the server" unless @envelope.signed_by?(server) | ||
end | ||
|
||
def client | ||
@client ||= begin | ||
auth_op = tx.operations&.first | ||
auth_op && Stellar::KeyPair.from_public_key(auth_op.source_account.ed25519!) | ||
end | ||
end | ||
|
||
def client_domain_account_address | ||
@client_domain_account_address = begin | ||
client_domain_account_op = tx.operations.find { |op| op.body.value.data_name == "client_domain" } | ||
client_domain_account_op && Util::StrKey.encode_muxed_account(client_domain_account_op.source_account) | ||
end | ||
end | ||
|
||
def verify_tx_signers(signers = []) | ||
raise ArgumentError, "no signers provided" if signers.empty? | ||
|
||
# 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 ArgumentError, "at least one regular signer must be provided" if client_signers.empty? | ||
|
||
raise InvalidChallengeError, "transaction has no signatures." if envelope.signatures.empty? | ||
|
||
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 InvalidChallengeError, "Transaction not signed by server: #{server.address}" | ||
end | ||
|
||
# Confirm we matched signatures to the client signers. | ||
if signers_found.empty? | ||
raise InvalidChallengeError, "Transaction not signed by any client signer." | ||
end | ||
|
||
# Confirm all signatures were consumed by a signer. | ||
if signers_found.size != envelope.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 | ||
|
||
private | ||
|
||
attr_reader :tx, :server | ||
|
||
def validate_tx! | ||
if tx.seq_num != 0 | ||
raise InvalidChallengeError, "The transaction sequence number should be zero" | ||
end | ||
|
||
if tx.source_account != server.muxed_account | ||
raise InvalidChallengeError, "The transaction source account is not equal to the server's account" | ||
end | ||
|
||
if tx.operations.size < 1 | ||
raise InvalidChallengeError, "The transaction should contain at least one operation" | ||
end | ||
|
||
time_bounds = tx.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 InvalidChallengeError, "The transaction has expired" | ||
end | ||
end | ||
|
||
def validate_operations!(**options) | ||
auth_op, *rest_ops = tx.operations | ||
client_account_id = auth_op.source_account | ||
|
||
auth_op_body = auth_op.body.value | ||
|
||
if client_account_id.blank? | ||
raise InvalidChallengeError, "The transaction's operation should contain a source account" | ||
end | ||
|
||
if auth_op.body.arm != :manage_data_op | ||
raise InvalidChallengeError, "The transaction's first operation should be manageData" | ||
end | ||
|
||
if options.key?(:domain) && auth_op_body.data_name != "#{options[:domain]} auth" | ||
raise InvalidChallengeError, "The transaction's operation data name is invalid" | ||
end | ||
|
||
if auth_op_body.data_value.unpack1("m").size != 48 | ||
raise InvalidChallengeError, "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 InvalidChallengeError, "The transaction has operations that are not of type 'manageData'" | ||
elsif op.source_account != server.muxed_account && op_params.data_name != "client_domain" | ||
raise InvalidChallengeError, "The transaction has operations that are unrecognized" | ||
elsif op_params.data_name == "web_auth_domain" && options.key?(:auth_domain) && op_params.data_value != options[:auth_domain] | ||
raise InvalidChallengeError, "The transaction has 'manageData' operation with 'web_auth_domain' key and invalid value" | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
module Stellar | ||
module Ecosystem | ||
module SEP10 | ||
class ChallengeTxBuilder | ||
def self.build(server:, client:, domain: nil, timeout: 300, **options) | ||
new(server: server, client: client, domain: domain, timeout: timeout, **options).build | ||
end | ||
|
||
def initialize(server:, client:, domain: nil, timeout: 300, **options) | ||
@server = server | ||
@client = client | ||
@timeout = timeout | ||
@domain = domain | ||
@options = options | ||
end | ||
|
||
def build | ||
tb = Stellar::TransactionBuilder.new( | ||
source_account: server, | ||
sequence_number: 0, | ||
time_bounds: time_bounds | ||
) | ||
|
||
tb.add_operation(main_operation) | ||
tb.add_operation(auth_domain_operation) if options.key?(:auth_domain) | ||
|
||
if options[:client_domain].present? | ||
if options[:client_domain_account].blank? | ||
raise "`client_domain_account` is required, if `client_domain` is provided" | ||
end | ||
|
||
tb.add_operation(client_domain_operation) | ||
end | ||
|
||
tb.build | ||
end | ||
|
||
private | ||
|
||
attr_reader :server, :client, :timeout, :domain, :options, :tx | ||
|
||
def time_bounds | ||
@time_bounds ||= begin | ||
now = Time.now.to_i | ||
Stellar::TimeBounds.new( | ||
min_time: now, | ||
max_time: now + timeout | ||
) | ||
end | ||
end | ||
|
||
def main_operation | ||
puts client.address | ||
Stellar::Operation.manage_data( | ||
name: "#{domain} auth", | ||
value: SecureRandom.base64(48), | ||
source_account: client | ||
) | ||
end | ||
|
||
def auth_domain_operation | ||
Stellar::Operation.manage_data( | ||
name: "web_auth_domain", | ||
value: options[:auth_domain], | ||
source_account: server | ||
) | ||
end | ||
|
||
def client_domain_operation | ||
Stellar::Operation.manage_data( | ||
name: "client_domain", | ||
value: options[:client_domain], | ||
source_account: options[:client_domain_account] | ||
) | ||
end | ||
end | ||
end | ||
end | ||
end |
Check notice
Code scanning / Remark-lint (reported by Codacy)
Warn when references to undefined definitions are found. Note