Module: Klay::Signature

Extended by:
Signature
Included in:
Signature
Defined in:
lib/klay/signature.rb

Overview

Defines handy tools for verifying and recovering signatures.

Defined Under Namespace

Classes: SignatureError

Constant Summary collapse

EIP191_PREFIX_BYTE =

EIP-191 prefix byte 0x19

"\x19".freeze
EIP712_VERSION_BYTE =

EIP-712 version byte 0x01

"\x01".freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.dissect(signature) ⇒ String

Dissects a signature blob of 65+ bytes into its r, s, and v values.

Parameters:

  • signature (String)

    a concatenated Secp256k1 signature string.

Returns:

  • (String, String, String)

    the r, s, and v values.

Raises:



50
51
52
53
54
55
56
57
58
59
60
# File 'lib/klay/signature.rb', line 50

def dissect(signature)
  signature = Util.bin_to_hex signature unless Util.is_hex? signature
  signature = Util.remove_hex_prefix signature
  if signature.size != 130
    raise SignatureError, "Unknown signature length #{signature.size}!"
  end
  r = signature[0...64]
  s = signature[64...128]
  v = signature[128..]
  return r, s, v
end

.personal_recover(message, signature, chain_id = Chain::CYPRESS) ⇒ String

Recovers a public key from a prefixed, personal message and a signature on a given chain. (EIP-191) Ref: https://eips.ethereum.org/EIPS/eip-191

Parameters:

  • message (String)

    the message string.

  • signature (String)

    the hex string containing the signature.

  • chain_id (Integer) (defaults to: Chain::CYPRESS)

    the chain ID the signature should be recovered from.

Returns:

  • (String)

    a hexa-decimal, uncompressed public key.



89
90
91
92
93
# File 'lib/klay/signature.rb', line 89

def personal_recover(message, signature, chain_id = Chain::CYPRESS)
  prefixed_message = prefix_message message
  hashed_message = Util.keccak256 prefixed_message
  recover hashed_message, signature, chain_id
end

.prefix_message(message) ⇒ String

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

Parameters:

  • message (String)

    the message string to be prefixed.

Returns:

  • (String)

    an EIP-191 prefixed string.



40
41
42
# File 'lib/klay/signature.rb', line 40

def prefix_message(message)
  "\u0019Klaytn Signed Message:\n#{message.size}#{message}"
end

.recover(blob, signature, chain_id = Chain::CYPRESS) ⇒ String

Recovers a signature from arbitrary data without validation on a given chain.

Parameters:

  • blob (String)

    that arbitrary data to be recovered.

  • signature (String)

    the hex string containing the signature.

  • chain_id (Integer) (defaults to: Chain::CYPRESS)

    the chain ID the signature should be recovered from.

Returns:

  • (String)

    a hexa-decimal, uncompressed public key.

Raises:



69
70
71
72
73
74
75
76
77
78
79
# File 'lib/klay/signature.rb', line 69

def recover(blob, signature, chain_id = Chain::CYPRESS)
  context = Secp256k1::Context.new
  r, s, v = dissect signature
  v = v.to_i(16)
  # raise SignatureError, "Invalid signature v byte #{v} for chain ID #{chain_id}!" if v != 130
  recovery_id = Chain.to_recovery_id v, chain_id
  signature_rs = Util.hex_to_bin "#{r}#{s}"
  recoverable_signature = context.recoverable_signature_from_compact signature_rs, recovery_id
  public_key = recoverable_signature.recover_public_key blob
  Util.bin_to_hex public_key.uncompressed
end

.recover_typed_data(typed_data, signature, chain_id = Chain::CYPRESS) ⇒ String

Recovers a public key from a typed data structure and a signature on a given chain. (EIP-712) Ref: https://eips.ethereum.org/EIPS/eip-712

Parameters:

  • typed_data (Array)

    all the data in the typed data structure to be recovered.

  • signature (String)

    the hex string containing the signature.

  • chain_id (Integer) (defaults to: Chain::CYPRESS)

    the chain ID the signature should be recovered from.

Returns:

  • (String)

    a hexa-decimal, uncompressed public key.



103
104
105
106
# File 'lib/klay/signature.rb', line 103

def recover_typed_data(typed_data, signature, chain_id = Chain::CYPRESS)
  hash_to_sign = Eip712.hash typed_data
  recover hash_to_sign, signature, chain_id
end

.verify(blob, signature, public_key, chain_id = Chain::CYPRESS) ⇒ Boolean

Verifies a signature for a given public key or address.

Parameters:

  • blob (String)

    that arbitrary data to be verified.

  • signature (String)

    the hex string containing the signature.

  • public_key (String)

    either a public key or an Klaytn address.

  • chain_id (Integer) (defaults to: Chain::CYPRESS)

    the chain ID used to sign.

Returns:

  • (Boolean)

    true if signature matches provided public key.

Raises:

  • (SignatureError)

    if it cannot determine the type of data or public key.



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/klay/signature.rb', line 116

def verify(blob, signature, public_key, chain_id = Chain::CYPRESS)
  recovered_key = nil
  if blob.instance_of? Array or blob.instance_of? Hash

    # recover Array from sign_typed_data
    recovered_key = recover_typed_data blob, signature, chain_id
  elsif blob.instance_of? String and blob.encoding != Encoding::ASCII_8BIT

    # recover message from personal_sign
    recovered_key = personal_recover blob, signature, chain_id
  elsif blob.instance_of? String and (Util.is_hex? blob or blob.encoding == Encoding::ASCII_8BIT)

    # if nothing else, recover from arbitrary signature
    recovered_key = recover blob, signature, chain_id
  end

  # raise if we cannot determine the data format
  raise SignatureError, "Unknown data format to verify: #{blob}" if recovered_key.nil?

  if public_key.instance_of? Address

    # recovering using an Klay::Address
    address = public_key.to_s
    recovered_address = Util.public_key_to_address(recovered_key).to_s
    return address == recovered_address
  elsif public_key.instance_of? Secp256k1::PublicKey

    # recovering using an Secp256k1::PublicKey
    public_hex = Util.bin_to_hex public_key.uncompressed
    return public_hex == recovered_key
  elsif public_key.size == 42

    # recovering using an address String
    address = Address.new(public_key).to_s
    recovered_address = Util.public_key_to_address(recovered_key).to_s
    return address == recovered_address
  elsif public_key.size == 130

    # recovering using an uncompressed public key String
    return public_key == recovered_key
  else

    # raise if we cannot determine the public key format used
    raise SignatureError, "Invalid public key or address supplied #{public_key}!"
  end
end

Instance Method Details

#dissect(signature) ⇒ String

Dissects a signature blob of 65+ bytes into its r, s, and v values.

Parameters:

  • signature (String)

    a concatenated Secp256k1 signature string.

Returns:

  • (String, String, String)

    the r, s, and v values.

Raises:



50
51
52
53
54
55
56
57
58
59
60
# File 'lib/klay/signature.rb', line 50

def dissect(signature)
  signature = Util.bin_to_hex signature unless Util.is_hex? signature
  signature = Util.remove_hex_prefix signature
  if signature.size != 130
    raise SignatureError, "Unknown signature length #{signature.size}!"
  end
  r = signature[0...64]
  s = signature[64...128]
  v = signature[128..]
  return r, s, v
end

#personal_recover(message, signature, chain_id = Chain::CYPRESS) ⇒ String

Recovers a public key from a prefixed, personal message and a signature on a given chain. (EIP-191) Ref: https://eips.ethereum.org/EIPS/eip-191

Parameters:

  • message (String)

    the message string.

  • signature (String)

    the hex string containing the signature.

  • chain_id (Integer) (defaults to: Chain::CYPRESS)

    the chain ID the signature should be recovered from.

Returns:

  • (String)

    a hexa-decimal, uncompressed public key.



89
90
91
92
93
# File 'lib/klay/signature.rb', line 89

def personal_recover(message, signature, chain_id = Chain::CYPRESS)
  prefixed_message = prefix_message message
  hashed_message = Util.keccak256 prefixed_message
  recover hashed_message, signature, chain_id
end

#prefix_message(message) ⇒ String

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

Parameters:

  • message (String)

    the message string to be prefixed.

Returns:

  • (String)

    an EIP-191 prefixed string.



40
41
42
# File 'lib/klay/signature.rb', line 40

def prefix_message(message)
  "\u0019Klaytn Signed Message:\n#{message.size}#{message}"
end

#recover(blob, signature, chain_id = Chain::CYPRESS) ⇒ String

Recovers a signature from arbitrary data without validation on a given chain.

Parameters:

  • blob (String)

    that arbitrary data to be recovered.

  • signature (String)

    the hex string containing the signature.

  • chain_id (Integer) (defaults to: Chain::CYPRESS)

    the chain ID the signature should be recovered from.

Returns:

  • (String)

    a hexa-decimal, uncompressed public key.

Raises:



69
70
71
72
73
74
75
76
77
78
79
# File 'lib/klay/signature.rb', line 69

def recover(blob, signature, chain_id = Chain::CYPRESS)
  context = Secp256k1::Context.new
  r, s, v = dissect signature
  v = v.to_i(16)
  # raise SignatureError, "Invalid signature v byte #{v} for chain ID #{chain_id}!" if v != 130
  recovery_id = Chain.to_recovery_id v, chain_id
  signature_rs = Util.hex_to_bin "#{r}#{s}"
  recoverable_signature = context.recoverable_signature_from_compact signature_rs, recovery_id
  public_key = recoverable_signature.recover_public_key blob
  Util.bin_to_hex public_key.uncompressed
end

#recover_typed_data(typed_data, signature, chain_id = Chain::CYPRESS) ⇒ String

Recovers a public key from a typed data structure and a signature on a given chain. (EIP-712) Ref: https://eips.ethereum.org/EIPS/eip-712

Parameters:

  • typed_data (Array)

    all the data in the typed data structure to be recovered.

  • signature (String)

    the hex string containing the signature.

  • chain_id (Integer) (defaults to: Chain::CYPRESS)

    the chain ID the signature should be recovered from.

Returns:

  • (String)

    a hexa-decimal, uncompressed public key.



103
104
105
106
# File 'lib/klay/signature.rb', line 103

def recover_typed_data(typed_data, signature, chain_id = Chain::CYPRESS)
  hash_to_sign = Eip712.hash typed_data
  recover hash_to_sign, signature, chain_id
end

#verify(blob, signature, public_key, chain_id = Chain::CYPRESS) ⇒ Boolean

Verifies a signature for a given public key or address.

Parameters:

  • blob (String)

    that arbitrary data to be verified.

  • signature (String)

    the hex string containing the signature.

  • public_key (String)

    either a public key or an Klaytn address.

  • chain_id (Integer) (defaults to: Chain::CYPRESS)

    the chain ID used to sign.

Returns:

  • (Boolean)

    true if signature matches provided public key.

Raises:

  • (SignatureError)

    if it cannot determine the type of data or public key.



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/klay/signature.rb', line 116

def verify(blob, signature, public_key, chain_id = Chain::CYPRESS)
  recovered_key = nil
  if blob.instance_of? Array or blob.instance_of? Hash

    # recover Array from sign_typed_data
    recovered_key = recover_typed_data blob, signature, chain_id
  elsif blob.instance_of? String and blob.encoding != Encoding::ASCII_8BIT

    # recover message from personal_sign
    recovered_key = personal_recover blob, signature, chain_id
  elsif blob.instance_of? String and (Util.is_hex? blob or blob.encoding == Encoding::ASCII_8BIT)

    # if nothing else, recover from arbitrary signature
    recovered_key = recover blob, signature, chain_id
  end

  # raise if we cannot determine the data format
  raise SignatureError, "Unknown data format to verify: #{blob}" if recovered_key.nil?

  if public_key.instance_of? Address

    # recovering using an Klay::Address
    address = public_key.to_s
    recovered_address = Util.public_key_to_address(recovered_key).to_s
    return address == recovered_address
  elsif public_key.instance_of? Secp256k1::PublicKey

    # recovering using an Secp256k1::PublicKey
    public_hex = Util.bin_to_hex public_key.uncompressed
    return public_hex == recovered_key
  elsif public_key.size == 42

    # recovering using an address String
    address = Address.new(public_key).to_s
    recovered_address = Util.public_key_to_address(recovered_key).to_s
    return address == recovered_address
  elsif public_key.size == 130

    # recovering using an uncompressed public key String
    return public_key == recovered_key
  else

    # raise if we cannot determine the public key format used
    raise SignatureError, "Invalid public key or address supplied #{public_key}!"
  end
end