Skip to content
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

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions base/lib/stellar/transaction_envelope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ def signed_correctly?(*key_pairs)
end
end

def signed_by?(keypair)
signatures.any? do |sig|
next if sig.hint != keypair.signature_hint

keypair.verify(sig.signature, tx.hash)
end
end

def merge(other)
merged_tx = tx.merge(other.tx)
merged_tx.signatures = [signatures, other.signatures]
Expand Down
20 changes: 20 additions & 0 deletions base/spec/lib/stellar/transaction_envelope_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,24 @@
subject { envelope.hash }
it { is_expected.to eq(Digest::SHA256.digest(envelope.tx.signature_base)) }
end

describe "#signed_by?" do
let(:keypair) { KeyPair() }

subject(:signed_by) do
envelope.signed_by?(keypair)
end

context "when envelope is signed by keypair" do
let(:signers) { [keypair] }

it { is_expected.to be_truthy }
end

context "when envelope is not signed by keypair" do
let(:signers) { [] }

it { is_expected.to be_falsey }
end
end
end
3 changes: 3 additions & 0 deletions ecosystem/.rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--format documentation
--color
--require spec_helper
5 changes: 5 additions & 0 deletions ecosystem/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## [Unreleased]

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

## [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
35 changes: 35 additions & 0 deletions ecosystem/README.md
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
10 changes: 10 additions & 0 deletions ecosystem/Rakefile
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]
15 changes: 15 additions & 0 deletions ecosystem/bin/console
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__)
8 changes: 8 additions & 0 deletions ecosystem/bin/setup
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
13 changes: 13 additions & 0 deletions ecosystem/lib/stellar-ecosystem.rb
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"
7 changes: 7 additions & 0 deletions ecosystem/lib/stellar/ecosystem/version.rb
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
163 changes: 163 additions & 0 deletions ecosystem/lib/stellar/sep10/challenge.rb
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
79 changes: 79 additions & 0 deletions ecosystem/lib/stellar/sep10/challenge_tx_builder.rb
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
Loading