Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@
/spec/reports/
/tmp/
.ruby-version

# Emacs
*~
\#*\#
.\#*
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ sudo: false
dist: xenial
language: ruby
rvm:
- 2.2.0
- 2.3.0
- 2.4.0
- 2.5.3
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,3 @@ The gem is available as open source under the terms of the [MIT License](http://

* Better test suite.
* Expose API for HD keys.
* Support signing with [libsecp256k1](https://github.com/bitcoin-core/secp256k1).
1 change: 1 addition & 0 deletions eth.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'digest-sha3-patched', '~> 1.1'
spec.add_dependency 'ffi', '~> 1.0'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
spec.add_dependency 'ffi', '~> 1.0'

I think we can get rid of the FFI dependency now that we aren't using OpenSSL.

spec.add_dependency 'money-tree', '~> 0.10.0'
spec.add_dependency 'rbsecp256k1', '~> 5.0.0'
spec.add_dependency 'rlp', '~> 0.7.3'
spec.add_dependency 'scrypt', '~> 3.0.6'

Expand Down
6 changes: 4 additions & 2 deletions lib/eth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
require 'ffi'
require 'money-tree'
require 'rlp'
require 'rbsecp256k1'

module Eth
BYTE_ZERO = "\x00".freeze
UINT_MAX = 2**256 - 1

autoload :Address, 'eth/address'
autoload :Chains, 'eth/chains'
autoload :Gas, 'eth/gas'
autoload :Key, 'eth/key'
autoload :OpenSsl, 'eth/open_ssl'
autoload :Secp256k1, 'eth/secp256k1'
autoload :PersonalMessage, 'eth/personal_message'
autoload :RecoverableSignature, 'eth/recoverable_signature'
autoload :Sedes, 'eth/sedes'
autoload :Tx, 'eth/tx'
autoload :Utils, 'eth/utils'
Expand Down
53 changes: 53 additions & 0 deletions lib/eth/chains.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
module Eth
# Encapsulates utilities and constants for various Ethereum chains.
module Chains
# Chain IDs for various chains (from EIP-155)
MAINNET = 1
MORDEN = 2
ROPSTEN = 3
RINKEBY = 4
KOVAN = 42
ETC_MAINNET = 61
ETC_TESTNET = 62

# Indicates whether or not the given value represents a legacy chain v.
#
# @return [Boolean] true if the v represents a signature before the ETC
# fork, false if it does not.
def self.legacy_recovery_id?(v)
[27, 28].include?(v)
end

# Convert a v value into an ECDSA recovery id.
#
# See EIP-155 for more information the computations done in this method:
# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
#
# @param v [Integer] v value from a signature.
# @param chain_id [Integer] chain ID for the chain the signature was
# generated on.
# @return [Integer] the recovery id corresponding to the given v value.
# @raise [ArgumentError] if the given v value is invalid.
def self.to_recovery_id(v, chain_id)
# Handle the legacy network recovery ids
return v - 27 if legacy_recovery_id?(v)

raise ArgumentError, "Invalid legacy v value #{v}." if chain_id.nil?

if [(2 * chain_id + 35), (2 * chain_id + 36)].include?(v)
return v - 35 - 2 * chain_id
end

raise ArgumentError, "Invalid v value for chain #{chain_id}. Invalid chain_id?"
end

# Converts a recovery ID into the expected v value.
#
# @param recovery_id [Integer] signature recovery id (should be 0 or 1).
# @param chain_id [Integer] Unique ID of the Ethereum chain.
# @return [Integer] the v value for the recovery id.
def self.to_v(recovery_id, chain_id)
2 * chain_id + 35 + recovery_id
end
end
end
97 changes: 74 additions & 23 deletions lib/eth/key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,47 @@ def self.decrypt(data, password)
new priv: priv
end

def self.personal_recover(message, signature)
bin_signature = Utils.hex_to_bin(signature).bytes.rotate(-1).pack('c*')
OpenSsl.recover_compact(Utils.keccak256(Utils.prefix_message(message)), bin_signature)
def self.recover_public_key(hash, signature, chain_id = nil)
context = Secp256k1::Context.new
v = signature.unpack('C').first
recovery_id = Eth::Chains.to_recovery_id(v, chain_id)
recoverable_signature = context.recoverable_signature_from_compact(
signature[1..-1], recovery_id
)
public_key_bin = recoverable_signature.recover_public_key(hash).uncompressed
Utils.bin_to_hex(public_key_bin)
rescue Secp256k1::DeserializationError
false
end

def self.personal_recover(message, signature, chain_id = nil)
hash = PersonalMessage.new(message).hash
bin_sig = Utils.hex_to_bin(signature).bytes.rotate(-1).pack('c*')
recover_public_key(hash, bin_sig, chain_id: chain_id)
end

def initialize(priv: nil)
@private_key = MoneyTree::PrivateKey.new key: priv
@public_key = MoneyTree::PublicKey.new private_key, compressed: false
@context = Secp256k1::Context.new
key_pair =
if priv.nil?
@context.generate_key_pair
else
@context.key_pair_from_private_key(Utils.hex_to_bin(priv))
end
@private_key = key_pair.private_key
@public_key = key_pair.public_key
end

def private_hex
private_key.to_hex
Utils.bin_to_hex(private_key.data)
end

def public_bytes
public_key.to_bytes
public_key.data
end

def public_hex
public_key.to_hex
Utils.bin_to_hex(public_key.uncompressed)
end

def address
Expand All @@ -47,33 +68,63 @@ def sign(message)
sign_hash message_hash(message)
end

def sign_hash(hash)
loop do
signature = OpenSsl.sign_compact hash, private_hex, public_hex
return signature if valid_s? signature
# Sign a data hash returning signature.
#
# @param hash [String] Keccak256 hash as byte string.
# @param chain_id [Integer] (Optional) ID of the chain message or
# transaction belongs to.
# @return [String] Recoverable signature as byte string
def sign_hash(hash, chain_id = nil)
if chain_id.nil?
sign_legacy(private_key, hash)
else
signature, recovery_id =
@context.sign_recoverable(private_key, hash).compact
result = signature.bytes
result.unshift(Eth::Chains.to_v(recovery_id, chain_id))
result.pack('c*')
end
end

def verify_signature(message, signature)
hash = message_hash(message)
public_hex == OpenSsl.recover_compact(hash, signature)
# Produces signature with legacy v values.
#
# @param private_key [Secp256k1::PrivateKey] signing key.
# @param hash [String] hash to be signed.
# @return [String] binary signature data.
def sign_legacy(private_key, hash)
signature, recovery_id =
@context.sign_recoverable(private_key, hash).compact
result = signature.bytes
result.unshift(Eth.v_base + recovery_id)
result.pack('c*')
end

def personal_sign(message)
Utils.bin_to_hex(sign(Utils.prefix_message(message)).bytes.rotate(1).pack('c*'))
def verify_signature(message, signature, chain_id = nil)
hash = message_hash(message)
v = signature.unpack('C')[0]
recovery_id = Eth::Chains.to_recovery_id(v, chain_id)
recoverable_signature = @context.recoverable_signature_from_compact(
signature[1..-1], recovery_id
)
public_key_bin =
recoverable_signature.recover_public_key(hash).uncompressed
public_hex == Utils.bin_to_hex(public_key_bin)
end

def personal_sign(message, chain_id = nil)
signature =
if chain_id
PersonalMessage.new(message).sign(private_key, nil)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that personal_sign did not take chain ID into account? As far as I can see in this PR, this chain_id parameter is is always nil for PersonalMessage#sign. Could we cut it? Is there a scenario for it I'm missing?

else
PersonalMessage.new(message).sign_legacy(private_key)
end
Secp256k1::Util.bin_to_hex(signature)
end

private

def message_hash(message)
Utils.keccak256 message
end

def valid_s?(signature)
s_value = Utils.v_r_s_for(signature).last
s_value <= Secp256k1::N/2 && s_value != 0
end

end
end
Loading