Skip to content

Commit

Permalink
lib/signature: implement personal_recover (eip 191) (#21)
Browse files Browse the repository at this point in the history
* lib/signature: implement personal_recover (eip 191)

* eth/chains: add edge case for ledger wallets

* spec: add another test case for mycrypto

* lib: clean up

* spec: clean up
  • Loading branch information
q9f committed Dec 14, 2021
1 parent 80c27fe commit 5303b25
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 33 deletions.
1 change: 1 addition & 0 deletions lib/eth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ module Eth
require 'eth/address'
require 'eth/chains'
require 'eth/key'
require 'eth/signature'
require 'eth/utils'
require 'eth/version'
8 changes: 1 addition & 7 deletions lib/eth/address.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,7 @@ def checksummed

Utils.prefix_hex(cased.join)
end

# Generate a checksummed address string. Alias for `checksummed`.
#
# @return [String] prefixed hexstring representing an checksummed address.
def to_s
checksummed
end
alias :to_s :checksummed

private

Expand Down
32 changes: 20 additions & 12 deletions lib/eth/chains.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,28 +93,36 @@ def is_legacy? v
# EIP-155 chain ID.
#
# @param v [Integer] the signature's `v` value
# @param chain [Integer] the chain id the signature was generated on.
# @param chain_id [Integer] the chain id the signature was generated on.
# @return [Integer] the recovery id corresponding to `v`.
# @raise [ArgumentError] if the given `v` is invalid.
def to_recov v, chain = ETHEREUM
x = 0 + 2 * chain + 35
y = 1 + 2 * chain + 35
if is_legacy? v
def to_recovery_id v, chain_id = ETHEREUM
e = 0 + 2 * chain_id + 35
i = 1 + 2 * chain_id + 35
if [0, 1].include? v

# some wallets are using a `v` of 0 or 1 (ledger)
return v
elsif is_legacy? v

# this is the pre-EIP-155 legacy case
return v - 27
elsif [x, y].include? v
return v - 35 - 2 * chain
elsif [e, i].include? v

# this is the EIP-155 case
return v - 35 - 2 * chain_id
else
raise ArgumentError, "Invalid v value for chain #{chain}. Invalid chain ID?"
raise ArgumentError, "Invalid v value for chain ID #{chain_id}. Invalid chain ID?"
end
end

# Converts a recovery ID into the expected `v` on a given chain.
#
# @param recov [Integer] signature recovery id.
# @param chain [Integer] the chain id the signature was generated on.
# @param recovery_id [Integer] signature recovery id.
# @param chain_id [Integer] the chain id the signature was generated on.
# @return [Integer] the signature's `v` value.
def to_v recov, chain = ETHEREUM
v = 2 * chain + 35 + recov
def to_v recovery_id, chain_id = ETHEREUM
v = 2 * chain_id + 35 + recovery_id
end
end
end
55 changes: 55 additions & 0 deletions lib/eth/signature.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright (c) 2016-2022 The Ruby-Eth Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'rbsecp256k1'

# Provides the `Eth` module.
module Eth

# Defines handy tools for verifying and recovering signatures.
module Signature
extend self

# Prefix message as per EIP-191 with 0x19 to ensure the data is not
# valid RLP and thus not mistaken for a transaction.
# EIP-191 Version byte: 0x45 (E)
# ref: https://eips.ethereum.org/EIPS/eip-191
#
# @param message [String] the message string to be prefixed.
# @return [String] an EIP-191 prefixed string
def prefix_message message
"\x19Ethereum Signed Message:\n#{message.size}#{message}"
end

# Recovers a uncompressed public key from a message and a signature
# on a given chain.
#
# @param message [String] the message string.
# @param signature [String] the hex string containing the signature.
# @param chain_id [Integer] the chain ID used to sign.
# @return [String] an uncompressed public key hex.
def personal_recover message, signature, chain_id = Chains::ETHEREUM
context = Secp256k1::Context.new
rotated_signature = Utils.hex_to_bin(signature).bytes.rotate -1
signature = rotated_signature[1..-1].pack 'c*'
v = rotated_signature.first
recovery_id = Chains.to_recovery_id v, chain_id
recoverable_signature = context.recoverable_signature_from_compact signature, recovery_id
prefixed_message = prefix_message message
hashed_message = Utils.keccak256 prefixed_message
public_key = recoverable_signature.recover_public_key hashed_message
Utils.bin_to_hex public_key.uncompressed
end
end
end
30 changes: 16 additions & 14 deletions spec/eth/chains_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,26 +57,28 @@
expect(Eth::Chains.to_v 0, Eth::Chains::PRIVATE_GETH).to be 2709
end
it "can recover v from ethereum recovery id" do
expect(Eth::Chains.to_recov 37).to be 0
expect(Eth::Chains.to_recov 38).to be 1
expect(Eth::Chains.to_recovery_id 37).to be 0
expect(Eth::Chains.to_recovery_id 38).to be 1

# legacy v
expect(Eth::Chains.to_recov 27).to be 0
expect(Eth::Chains.to_recov 28).to be 1
expect(Eth::Chains.to_recovery_id 0).to be 0
expect(Eth::Chains.to_recovery_id 1).to be 1
expect(Eth::Chains.to_recovery_id 27).to be 0
expect(Eth::Chains.to_recovery_id 28).to be 1
end
it "can recover v from other chain's recovery id" do
expect(Eth::Chains.to_recov 157, Eth::Chains::CLASSIC).to be 0
expect(Eth::Chains.to_recov 236, Eth::Chains::XDAI).to be 1
expect(Eth::Chains.to_recov 84357, Eth::Chains::ARBITRUM).to be 0
expect(Eth::Chains.to_recov 160, Eth::Chains::MORDEN_CLASSIC).to be 1
expect(Eth::Chains.to_recov 875, Eth::Chains::GOERLI_OPTIMISM).to be 0
expect(Eth::Chains.to_recov 843258, Eth::Chains::RINKEBY_ARBITRUM).to be 1
expect(Eth::Chains.to_recov 2709, Eth::Chains::PRIVATE_GETH).to be 0
expect(Eth::Chains.to_recovery_id 157, Eth::Chains::CLASSIC).to be 0
expect(Eth::Chains.to_recovery_id 236, Eth::Chains::XDAI).to be 1
expect(Eth::Chains.to_recovery_id 84357, Eth::Chains::ARBITRUM).to be 0
expect(Eth::Chains.to_recovery_id 160, Eth::Chains::MORDEN_CLASSIC).to be 1
expect(Eth::Chains.to_recovery_id 875, Eth::Chains::GOERLI_OPTIMISM).to be 0
expect(Eth::Chains.to_recovery_id 843258, Eth::Chains::RINKEBY_ARBITRUM).to be 1
expect(Eth::Chains.to_recovery_id 2709, Eth::Chains::PRIVATE_GETH).to be 0
end
it "raises an error for invalid v on chain ids" do
expect {Eth::Chains.to_recov 0}.to raise_error ArgumentError
expect {Eth::Chains.to_recov 36}.to raise_error ArgumentError
expect {Eth::Chains.to_recov 843258, Eth::Chains::PRIVATE_GETH}.to raise_error ArgumentError
expect {Eth::Chains.to_recovery_id -1}.to raise_error ArgumentError
expect {Eth::Chains.to_recovery_id 36}.to raise_error ArgumentError
expect {Eth::Chains.to_recovery_id 843258, Eth::Chains::PRIVATE_GETH}.to raise_error ArgumentError
end
end
end
33 changes: 33 additions & 0 deletions spec/eth/signature_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require 'spec_helper'

describe Eth::Signature do
describe ".personal_recover" do

it "can recover a public key from a signature generated with metamask" do
message = "test"
signature = "3eb24bd327df8c2b614c3f652ec86efe13aa721daf203820241c44861a26d37f2bffc6e03e68fc4c3d8d967054c9cb230ed34339b12ef89d512b42ae5bf8c2ae1c"
public_hex = "043e5b33f0080491e21f9f5f7566de59a08faabf53edbc3c32aaacc438552b25fdde531f8d1053ced090e9879cbf2b0d1c054e4b25941dab9254d2070f39418afc"
expect(Eth::Signature.personal_recover(message, signature).to_s).to eq public_hex
end

it "can recover a public key from a signature generated with ledger" do
message = "test"
signature = "0x5c433983b23738940ce256c59d5bc6a3d5fd12c5bc9bdbf0ffdffb7be1a09d1815ca1db167c61a10945837f3fb4821086d6656b4fa6ede9c4d1aeaf07e2b0adf01"
public_hex = "04e51ff5abc511f2fda0f893c10054123e92527b5e69e24cca538e74edbd604508259e1b265b54628bc8024fb791e459f67adb770b20962eb38fabe8b86f2aebaa"
expect(Eth::Signature.personal_recover message, signature).to eq public_hex
end

it "can recover an address from a signature generated with mycrypto" do
alice = Eth::Address.new '0x4fCA53a6658648060e0a1Ca8427Abdd6063eDf6A'
message = "Hello World!"
signature = "0x21fbf0696d5e0aa2ef41a2b4ffb623bcaf070461d61cf7251c74161f82fec3a4370854bc0a34b3ab487c1bc021cd318c734c51ae29374f2beb0e6f2dd49b4bf41c"
expect(Eth::Utils.public_key_to_address(Eth::Signature.personal_recover(message, signature)).to_s).to eq alice.to_s

# ref: https://support.mycrypto.com/how-to/getting-started/how-to-sign-and-verify-messages-on-ethereum/
bob = Eth::Address.new '0x2a3052ef570a031400BffD61438b2D19e0E8abef'
message = "This is proof that I, user A, have access to this address."
signature = "0x4e1ce8ea60bc6dfd4068a35462612495850cb645a1c9f475eb969bff21d0b0fb414112aaf13f01dd18a3527cb648cdd51b618ae49d4999112c33f86b7b26e9731b"
expect(Eth::Utils.public_key_to_address(Eth::Signature.personal_recover(message, signature)).to_s).to eq bob.to_s
end
end
end

0 comments on commit 5303b25

Please sign in to comment.