Class: DEVp2p::RLPxSession

Inherits:
Object
  • Object
show all
Extended by:
Configurable
Defined in:
lib/devp2p/rlpx_session.rb

Constant Summary collapse

SUPPORTED_RLPX_VERSION =
4
ENC_CIPHER =
'AES-256-CTR'
MAC_CIPHER =
'AES-256-ECB'

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Configurable

add_config

Constructor Details

#initialize(ecc, is_initiator = false, ephemeral_privkey = nil) ⇒ RLPxSession

Returns a new instance of RLPxSession.



39
40
41
42
43
44
45
46
# File 'lib/devp2p/rlpx_session.rb', line 39

def initialize(ecc, is_initiator=false, ephemeral_privkey=nil)
  @ecc = ecc
  @is_initiator = is_initiator
  @ephemeral_ecc = Crypto::ECCx.new ephemeral_privkey

  @ready = false
  @got_eip8_auth, @got_eip8_ack = false, false
end

Instance Attribute Details

#eccObject (readonly)

Returns the value of attribute ecc.



35
36
37
# File 'lib/devp2p/rlpx_session.rb', line 35

def ecc
  @ecc
end

#ephemeral_eccObject (readonly)

Returns the value of attribute ephemeral_ecc.



35
36
37
# File 'lib/devp2p/rlpx_session.rb', line 35

def ephemeral_ecc
  @ephemeral_ecc
end

#initiator_nonceObject (readonly)

Returns the value of attribute initiator_nonce.



35
36
37
# File 'lib/devp2p/rlpx_session.rb', line 35

def initiator_nonce
  @initiator_nonce
end

#remote_ephemeral_pubkeyObject (readonly)

Returns the value of attribute remote_ephemeral_pubkey.



35
36
37
# File 'lib/devp2p/rlpx_session.rb', line 35

def remote_ephemeral_pubkey
  @remote_ephemeral_pubkey
end

#remote_pubkeyObject (readonly)

Returns the value of attribute remote_pubkey.



35
36
37
# File 'lib/devp2p/rlpx_session.rb', line 35

def remote_pubkey
  @remote_pubkey
end

#remote_versionObject (readonly)

Returns the value of attribute remote_version.



35
36
37
# File 'lib/devp2p/rlpx_session.rb', line 35

def remote_version
  @remote_version
end

#responder_nonceObject (readonly)

Returns the value of attribute responder_nonce.



35
36
37
# File 'lib/devp2p/rlpx_session.rb', line 35

def responder_nonce
  @responder_nonce
end

Instance Method Details

#aes_dec(data = '') ⇒ Object



335
336
337
# File 'lib/devp2p/rlpx_session.rb', line 335

def aes_dec(data='')
  @aes_dec.update data
end

#aes_enc(data = '') ⇒ Object



331
332
333
# File 'lib/devp2p/rlpx_session.rb', line 331

def aes_enc(data='')
  @aes_enc.update data
end

#create_auth_ack_message(ephemeral_pubkey = nil, nonce = nil, version = SUPPORTED_RLPX_VERSION, eip8 = false) ⇒ Object

authRecipient = E(remote-pubk, remote-ephemeral-pubk || nonce || 0x1) // token found authRecipient = E(remote-pubk, remote-ephemeral-pubk || nonce || 0x0) // token not found

nonce, ephemeral_pubkey, version are local

Raises:



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/devp2p/rlpx_session.rb', line 208

def create_auth_ack_message(ephemeral_pubkey=nil, nonce=nil, version=SUPPORTED_RLPX_VERSION, eip8=false)
  raise RLPxSessionError, 'must not be initiator' if initiator?

  ephemeral_pubkey = ephemeral_pubkey || @ephemeral_ecc.raw_pubkey
  @responder_nonce = nonce || Crypto.keccak256(Utils.int_to_big_endian(SecureRandom.random_number(TT256)))

  if eip8 || @got_eip8_auth
    msg = create_eip8_auth_ack_message ephemeral_pubkey, @responder_nonce, version
    raise RLPxSessionError, 'invalid msg size' unless msg.size > 97
  else
    msg = "#{ephemeral_pubkey}#{@responder_nonce}\x00"
    raise RLPxSessionError, 'invalid msg size' unless msg.size == 97
  end

  msg
end

#create_auth_message(remote_pubkey, ephemeral_privkey = nil, nonce = nil) ⇒ Object

  1. initiator generates ecdhe-random and nonce and creates auth

  2. initiator connects to remote and sends auth

New:

E(remote-pubk,
  S(ephemeral-privk, ecdh-shared-secret ^ nonce) ||
  H(ephemeral-pubk) || pubk || nonce || 0x0
)

Known:

E(remote-pubk,
  S(ephemeral-privk, token ^ nonce) ||
  H(ephemeral-pubk) || pubk || nonce || 0x1
)

Raises:



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
# File 'lib/devp2p/rlpx_session.rb', line 128

def create_auth_message(remote_pubkey, ephemeral_privkey=nil, nonce=nil)
  raise RLPxSessionError, 'must be initiator' unless initiator?
  raise InvalidKeyError, 'invalid remote pubkey' unless Crypto::ECCx.valid_key?(remote_pubkey)

  @remote_pubkey = remote_pubkey

  token = @ecc.get_ecdh_key remote_pubkey
  flag = 0x0

  @initiator_nonce = nonce || Crypto.keccak256(Utils.int_to_big_endian(SecureRandom.random_number(TT256)))
  raise RLPxSessionError, 'invalid nonce length' unless @initiator_nonce.size == 32

  token_xor_nonce = Utils.sxor token, @initiator_nonce
  raise RLPxSessionError, 'invalid token xor nonce length' unless token_xor_nonce.size == 32

  ephemeral_pubkey = @ephemeral_ecc.raw_pubkey
  raise InvalidKeyError, 'invalid ephemeral pubkey' unless ephemeral_pubkey.size == 512 / 8 && Crypto::ECCx.valid_key?(ephemeral_pubkey)

  sig = @ephemeral_ecc.sign token_xor_nonce
  raise RLPxSessionError, 'invalid signature' unless sig.size == 65

  auth_message = "#{sig}#{Crypto.keccak256(ephemeral_pubkey)}#{@ecc.raw_pubkey}#{@initiator_nonce}#{flag.chr}"
  raise RLPxSessionError, 'invalid auth message length' unless auth_message.size == 194

  auth_message
end

#create_eip8_auth_ack_message(ephemeral_pubkey, nonce, version) ⇒ Object



225
226
227
228
229
# File 'lib/devp2p/rlpx_session.rb', line 225

def create_eip8_auth_ack_message(ephemeral_pubkey, nonce, version)
  data = RLP.encode [ephemeral_pubkey, nonce, version], sedes: eip8_ack_sedes
  pad = SecureRandom.random_bytes(SecureRandom.random_number(151)+100) # (100..150) random bytes
  "#{data}#{pad}"
end

#decode_auth_ack_message(ciphertext) ⇒ Object

Raises:



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/devp2p/rlpx_session.rb', line 248

def decode_auth_ack_message(ciphertext)
  raise RLPxSessionError, 'must be initiator' unless initiator?
  raise ArgumentError, 'invalid ciphertext length' unless ciphertext.size >= 210

  result = nil
  begin
    result = decode_ack_plain ciphertext
  rescue AuthenticationError
    result = decode_ack_eip8 ciphertext
    @got_eip8_ack = true
  end
  size, ephemeral_pubkey, nonce, version = result

  @auth_ack = ciphertext[0,size]
  @remote_ephemeral_pubkey = ephemeral_pubkey[0,64]
  @responder_nonce = nonce
  @remote_version = version

  raise InvalidKeyError, 'invalid remote ephemeral pubkey' unless Crypto::ECCx.valid_key?(@remote_ephemeral_pubkey)

  ciphertext[size..-1]
end

#decode_authentication(ciphertext) ⇒ Object

  1. optionally, remote decrypts and verifies auth (checks that recovery of

signature == H(ephemeral-pubk))
  1. remote generates authAck from remote-ephemeral-pubk and nonce (authAck

= authRecipient handshake)

optional: remote derives secrets and preemptively sends protocol-handshake (steps 9,11,8,10)

Raises:



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/devp2p/rlpx_session.rb', line 174

def decode_authentication(ciphertext)
  raise RLPxSessionError, 'must not be initiator' if initiator?
  raise ArgumentError, 'invalid ciphertext length' unless ciphertext.size >= 307

  result = nil
  begin
    result = decode_auth_plain ciphertext
  rescue AuthenticationError
    result = decode_auth_eip8 ciphertext
    @got_eip8_auth = true
  end
  size, sig, initiator_pubkey, nonce, version = result

  @auth_init = ciphertext[0, size]

  token = @ecc.get_ecdh_key initiator_pubkey
  @remote_ephemeral_pubkey = Crypto.ecdsa_recover(Utils.sxor(token, nonce), sig)
  raise InvalidKeyError, 'invalid remote ephemeral pubkey' unless Crypto::ECCx.valid_key?(@remote_ephemeral_pubkey)

  @initiator_nonce = nonce
  @remote_pubkey = initiator_pubkey
  @remote_version = version

  ciphertext[size..-1]
end

#decrypt(data) ⇒ Object

Raises:



97
98
99
100
101
102
103
104
105
106
# File 'lib/devp2p/rlpx_session.rb', line 97

def decrypt(data)
  header = decrypt_header data[0,32]
  body_size = Frame.decode_body_size header

  len = 32 + Utils.ceil16(body_size) + 16
  raise FormatError, 'insufficient body length' unless data.size >= len

  frame = decrypt_body data[32..-1], body_size
  {header: header, frame: frame, bytes_read: len}
end

#decrypt_body(data, body_size) ⇒ Object

Raises:



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/devp2p/rlpx_session.rb', line 80

def decrypt_body(data, body_size)
  raise RLPxSessionError, 'not ready' unless ready?

  read_size = Utils.ceil16 body_size
  raise FormatError, 'insufficient body length' unless data.size >= read_size + 16

  frame_ciphertext = data[0, read_size]
  frame_mac = data[read_size, 16]
  raise RLPxSessionError, 'invalid frame mac length' unless frame_mac.size == 16

  fmac_seed = ingress_mac frame_ciphertext
  expected_frame_mac = ingress_mac(Utils.sxor(mac_enc(ingress_mac[0,16]), fmac_seed[0,16]))[0,16]
  raise AuthenticationError, 'invalid frame mac' unless expected_frame_mac == frame_mac

  aes_dec(frame_ciphertext)[0,body_size]
end

#decrypt_header(data) ⇒ Object

Raises:



67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/devp2p/rlpx_session.rb', line 67

def decrypt_header(data)
  raise RLPxSessionError, 'not ready' unless ready?
  raise ArgumentError, 'invalid data length' unless data.size == 32

  header_ciphertext = data[0,16]
  header_mac = data[16,16]

  expected_header_mac = ingress_mac(Utils.sxor(mac_enc(ingress_mac[0,16]), header_ciphertext))[0,16]
  raise AuthenticationError, 'invalid header mac' unless expected_header_mac == header_mac

  aes_dec header_ciphertext
end

#egress_mac(data = '') ⇒ Object



339
340
341
342
# File 'lib/devp2p/rlpx_session.rb', line 339

def egress_mac(data='')
  @egress_mac.update data
  return @egress_mac.digest
end

#encrypt(header, frame) ⇒ Object

Frame Handling

Raises:



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/devp2p/rlpx_session.rb', line 50

def encrypt(header, frame)
  raise RLPxSessionError, 'not ready' unless ready?
  raise ArgumentError, 'invalid header length' unless header.size == 16
  raise ArgumentError, 'invalid frame padding' unless frame.size % 16 == 0

  header_ciphertext = aes_enc header
  raise RLPxSessionError unless header_ciphertext.size == header.size
  header_mac = egress_mac(Utils.sxor(mac_enc(egress_mac[0,16]), header_ciphertext))[0,16]

  frame_ciphertext = aes_enc frame
  raise RLPxSessionError unless frame_ciphertext.size == frame.size
  fmac_seed = egress_mac frame_ciphertext
  frame_mac = egress_mac(Utils.sxor(mac_enc(egress_mac[0,16]), fmac_seed[0,16]))[0,16]

  header_ciphertext + header_mac + frame_ciphertext + frame_mac
end

#encrypt_auth_ack_message(ack_message, eip8 = false, remote_pubkey = nil) ⇒ Object

Raises:



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/devp2p/rlpx_session.rb', line 231

def encrypt_auth_ack_message(ack_message, eip8=false, remote_pubkey=nil)
  raise RLPxSessionError, 'must not be initiator' if initiator?

  remote_pubkey ||= @remote_pubkey

  if eip8 || @got_eip8_auth
    # The EIP-8 version has an authenticated length prefix
    prefix = [ack_message.size + Crypto::ECIES::ENCRYPT_OVERHEAD_LENGTH].pack("S>")
    @auth_ack = "#{prefix}#{@ecc.ecies_encrypt(ack_message, remote_pubkey, prefix)}"
  else
    @auth_ack = @ecc.ecies_encrypt ack_message, remote_pubkey
    raise RLPxSessionError, 'invalid auth ack message length' unless @auth_ack.size == 210
  end

  @auth_ack
end

#encrypt_auth_message(auth_message, remote_pubkey = nil) ⇒ Object

Raises:



155
156
157
158
159
160
161
162
163
# File 'lib/devp2p/rlpx_session.rb', line 155

def encrypt_auth_message(auth_message, remote_pubkey=nil)
  raise RLPxSessionError, 'must be initiator' unless initiator?

  remote_pubkey ||= @remote_pubkey
  @auth_init = @ecc.ecies_encrypt auth_message, remote_pubkey
  raise RLPxSessionError, 'invalid encrypted auth message length' unless @auth_init.size == 307

  @auth_init
end

#ingress_mac(data = '') ⇒ Object



344
345
346
347
# File 'lib/devp2p/rlpx_session.rb', line 344

def ingress_mac(data='')
  @ingress_mac.update data
  return @ingress_mac.digest
end

#initiator?Boolean

Returns:

  • (Boolean)


323
324
325
# File 'lib/devp2p/rlpx_session.rb', line 323

def initiator?
  @is_initiator
end

#mac_enc(data) ⇒ Object



327
328
329
# File 'lib/devp2p/rlpx_session.rb', line 327

def mac_enc(data)
  @mac_enc.update data
end

#ready?Boolean

Helpers

Returns:

  • (Boolean)


319
320
321
# File 'lib/devp2p/rlpx_session.rb', line 319

def ready?
  @ready
end

#setup_cipherObject

Handshake Key Derivation

Raises:



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/devp2p/rlpx_session.rb', line 273

def setup_cipher
  raise RLPxSessionError, 'missing responder nonce' unless @responder_nonce
  raise RLPxSessionError, 'missing initiator_nonce' unless @initiator_nonce
  raise RLPxSessionError, 'missing auth_init' unless @auth_init
  raise RLPxSessionError, 'missing auth_ack' unless @auth_ack
  raise RLPxSessionError, 'missing remote ephemeral pubkey' unless @remote_ephemeral_pubkey
  raise InvalidKeyError, 'invalid remote ephemeral pubkey' unless Crypto::ECCx.valid_key?(@remote_ephemeral_pubkey)

  # derive base secrets from ephemeral key agreement
  # ecdhe-shared-secret = ecdh.agree(ephemeral-privkey, remote-ephemeral-pubk)
  @ecdhe_shared_secret = @ephemeral_ecc.get_ecdh_key(@remote_ephemeral_pubkey)
  @shared_secret = Crypto.keccak256("#{@ecdhe_shared_secret}#{Crypto.keccak256(@responder_nonce + @initiator_nonce)}")
  @token = Crypto.keccak256 @shared_secret
  @aes_secret = Crypto.keccak256 "#{@ecdhe_shared_secret}#{@shared_secret}"
  @mac_secret = Crypto.keccak256 "#{@ecdhe_shared_secret}#{@aes_secret}"

  mac1 = keccak256 "#{Utils.sxor(@mac_secret, @responder_nonce)}#{@auth_init}"
  mac2 = keccak256 "#{Utils.sxor(@mac_secret, @initiator_nonce)}#{@auth_ack}"

  if initiator?
    @egress_mac, @ingress_mac = mac1, mac2
  else
    @egress_mac, @ingress_mac = mac2, mac1
  end

  iv = "\x00" * 16
  @aes_enc = OpenSSL::Cipher.new(ENC_CIPHER).tap do |c|
    c.encrypt
    c.iv = iv
    c.key = @aes_secret
  end
  @aes_dec = OpenSSL::Cipher.new(ENC_CIPHER).tap do |c|
    c.decrypt
    c.iv = iv
    c.key = @aes_secret
  end
  @mac_enc = OpenSSL::Cipher.new(MAC_CIPHER).tap do |c|
    c.encrypt
    c.key = @mac_secret
  end

  @ready = true
end