def test_ecies_decrypt(): tv = test_values from devp2p.crypto import ECCx e = ECCx(raw_privkey=tv['receiver_private_key']) _dec = e.ecies_decrypt(tv['auth_ciphertext']) assert len(_dec) == len(tv['auth_plaintext']) assert _dec == tv['auth_plaintext']
def test_session(): initiator = RLPxSession(ECCx(raw_privkey=mk_privkey('secret1')), is_initiator=True) initiator_pubk = initiator.ecc.raw_pubkey responder = RLPxSession(ECCx(raw_privkey=mk_privkey('secret2'))) responder_pubk = responder.ecc.raw_pubkey auth_msg = initiator.create_auth_message(remote_pubkey=responder_pubk) auth_msg_ct = initiator.encrypt_auth_message(auth_msg, responder_pubk) responder.decode_authentication(auth_msg_ct) auth_ack_msg = responder.create_auth_ack_message() auth_ack_msg_ct = responder.encrypt_auth_ack_message( auth_ack_msg, initiator_pubk) initiator.decode_auth_ack_message(auth_ack_msg_ct) initiator.setup_cipher() responder.setup_cipher() assert responder.ecdhe_shared_secret == initiator.ecdhe_shared_secret assert responder.token == initiator.token assert responder.aes_secret == initiator.aes_secret assert responder.mac_secret == initiator.mac_secret assert responder.egress_mac.digest() == initiator.ingress_mac.digest() assert responder.egress_mac.digest() == initiator.ingress_mac.digest() assert responder.ingress_mac.digest() == initiator.egress_mac.digest() assert responder.ingress_mac.digest() == initiator.egress_mac.digest() assert responder.mac_secret == initiator.mac_secret return initiator, responder
def __init__(self, app): super(RNOService, self).__init__(app) log.info('Initializing RNO') self.config = app.config self.interrupt = Event() self.tx_queue = Queue() # thread safe self.privkey_hex = self.config['eth']['privkey_hex'].decode('hex') self.my_addr = privtoaddr(self.privkey_hex) self.eccx = ECCx(None, self.privkey_hex)
def __init__(self, ecc, is_initiator=False, token_by_pubkey=dict(), ephemeral_privkey=None): self.ecc = ecc self.is_initiator = is_initiator self.token_by_pubkey = token_by_pubkey self.ephemeral_ecc = ECCx(raw_privkey=ephemeral_privkey)
def test_auth_ack_is_eip8_for_eip8_auth(): responder = RLPxSession(ECCx(raw_privkey=eip8_values['key_b'])) responder.decode_authentication(eip8_handshakes[1]['auth']) assert responder.got_eip8_auth ack = responder.create_auth_ack_message(version=55) ack_ct = responder.encrypt_auth_ack_message(ack) initiator = RLPxSession(ECCx(raw_privkey=eip8_values['key_a']), is_initiator=True) initiator.decode_auth_ack_message(ack_ct) assert initiator.got_eip8_ack assert initiator.remote_version == 55
def create_auth_message(self, remote_pubkey, token=None, ephemeral_privkey=None, nonce=None): """ 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) """ if not token: # new ecdh_shared_secret = self.node.get_ecdh_key(remote_pubkey) token = ecdh_shared_secret flag = 0x0 else: flag = 0x1 nonce = nonce or ienc(random.randint(0, 2**256 - 1)) assert len(nonce) == 32 token_xor_nonce = sxor(token, nonce) assert len(token_xor_nonce) == 32 # generate session ephemeral key if not ephemeral_privkey: ephemeral_privkey = sha3(ienc(random.randint(0, 2**256 - 1))) self.ephemeral_ecc = ECCx(raw_privkey=ephemeral_privkey) ephemeral_pubkey = self.ephemeral_ecc.raw_pubkey assert len(ephemeral_pubkey) == 512 / 8 # S(ephemeral-privk, ecdh-shared-secret ^ nonce) S = self.ephemeral_ecc.sign(token_xor_nonce) assert len(S) == 65 # S || H(ephemeral-pubk) || pubk || nonce || 0x0 auth_message = S + sha3( ephemeral_pubkey) + self.node.raw_pubkey + nonce + chr(flag) assert len(auth_message) == 65 + 32 + 64 + 32 + 1 == 194 return auth_message
def test_pyelliptic_sig(): priv_seed = 'test' priv_key = mk_privkey(priv_seed) my_pubkey = privtopub(priv_key) e = ECCx(my_pubkey, priv_key) msg = 'a' s = pyelliptic.ECC.sign(e, msg) assert s == pyelliptic.ECC.sign(e, msg) # deterministic
def test_eip8_handshake_messages(): initiator = RLPxSession(ECCx(raw_privkey=eip8_values['key_a']), is_initiator=True) responder = RLPxSession(ECCx(raw_privkey=eip8_values['key_b'])) for handshake in eip8_handshakes: ack_rest = initiator.decode_auth_ack_message(handshake['ack']) assert initiator.remote_ephemeral_pubkey == eip8_values['eph_pub_b'] assert initiator.responder_nonce == eip8_values['nonce_b'] assert initiator.got_eip8_ack == handshake['eip8_format'] assert initiator.remote_version == handshake['ack_version'] assert ack_rest == b'' auth_rest = responder.decode_authentication(handshake['auth']) assert responder.remote_ephemeral_pubkey == eip8_values['eph_pub_a'] assert responder.initiator_nonce == eip8_values['nonce_a'] assert responder.remote_pubkey == eip8_values['pub_a'] assert responder.got_eip8_auth == handshake['eip8_format'] assert auth_rest == b''
def test_pyelliptic_sig(): priv_seed = 'test' priv_key = mk_privkey(priv_seed) my_pubkey = privtopub(priv_key) e = ECCx(raw_privkey=priv_key) msg = 'a' s = pyelliptic.ECC.sign(e, msg) s2 = pyelliptic.ECC.sign(e, msg) assert s != s2 # non deterministic
class LocalNode(object): def __init__(self, privkey): self.ecc = ECCx(raw_privkey=privkey) @property def pubkey(self): return self.ecc.pubkey_x + self.ecc.pubkey_y def sign(self, data): return self.ecc.sign(data)
def test_eip8_key_derivation(): responder = RLPxSession(ECCx(raw_privkey=eip8_values['key_b']), ephemeral_privkey=eip8_values['eph_key_b']) responder.decode_authentication(eip8_handshakes[1]['auth']) ack = responder.create_auth_ack_message(nonce=eip8_values['nonce_b']) responder.encrypt_auth_ack_message(ack) responder.setup_cipher() want_aes_secret = decode_hex( b'80e8632c05fed6fc2a13b0f8d31a3cf645366239170ea067065aba8e28bac487') want_mac_secret = decode_hex( b'2ea74ec5dae199227dff1af715362700e989d889d7a493cb0639691efb8e5f98') assert responder.aes_secret == want_aes_secret assert responder.mac_secret == want_mac_secret responder.ingress_mac.update(b'foo') mac_digest = responder.ingress_mac.digest() want_mac_digest = decode_hex( b'0c7ec6340062cc46f5e9f1e3cf86f8c8c403c5a0964f5df0ebd34a75ddc86db5') assert mac_digest == want_mac_digest
class RLPxSession(object): ephemeral_ecc = None remote_ephemeral_pubkey = None initiator_nonce = None responder_nonce = None auth_init = None auth_ack = None aes_secret = None token = None aes_enc = None aes_dec = None mac_enc = None egress_mac = None ingress_mac = None is_ready = False remote_token_found = False remote_pubkey = None auth_message_length = 194 auth_message_ct_length = auth_message_length + ECCx.ecies_encrypt_overhead_length auth_ack_message_length = 97 auth_ack_message_ct_length = auth_ack_message_length + ECCx.ecies_encrypt_overhead_length def __init__(self, ecc, is_initiator=False, token_by_pubkey=dict(), ephemeral_privkey=None): self.ecc = ecc self.is_initiator = is_initiator self.token_by_pubkey = token_by_pubkey self.ephemeral_ecc = ECCx(raw_privkey=ephemeral_privkey) def encrypt(self, header, frame): """ # https://github.com/ethereum/go-ethereum/blob/develop/p2p/rlpx.go # https://github.com/ethereum/cpp-ethereum/blob/develop/libp2p/RLPxFrameIO.cpp """ assert self.is_ready is True assert len(header) == 16 assert len(frame) % 16 == 0 def aes(data=''): return self.aes_enc.update(data) def mac(data=''): self.egress_mac.update(data) return self.egress_mac.digest() # header header_ciphertext = aes(header) assert len(header_ciphertext) == 16 # egress-mac.update(aes(mac-secret,egress-mac) ^ header-ciphertext).digest header_mac = mac(sxor(self.mac_enc(mac()[:16]), header_ciphertext))[:16] # frame frame_ciphertext = aes(frame) assert len(frame_ciphertext) == len(frame) # egress-mac.update(aes(mac-secret,egress-mac) ^ # left128(egress-mac.update(frame-ciphertext).digest)) fmac_seed = mac(frame_ciphertext) frame_mac = mac(sxor(self.mac_enc(mac()[:16]), fmac_seed[:16]))[:16] return header_ciphertext + header_mac + frame_ciphertext + frame_mac def decrypt_header(self, data): assert self.is_ready is True assert len(data) == 32 def aes(data=''): return self.aes_dec.update(data) def mac(data=''): self.ingress_mac.update(data) return self.ingress_mac.digest() header_ciphertext = data[:16] header_mac = data[16:32] # FIXME: how to restore mac if i received invalid data? # ingress-mac.update(aes(mac-secret,ingress-mac) ^ header-ciphertext).digest expected_header_mac = mac( sxor(self.mac_enc(mac()[:16]), header_ciphertext))[:16] # expected_header_mac = self.updateMAC(self.ingress_mac, header_ciphertext) if not expected_header_mac == header_mac: raise AuthenticationError('invalid header mac') return aes(header_ciphertext) def decrypt_body(self, data, body_size): assert self.is_ready is True def aes(data=''): return self.aes_dec.update(data) def mac(data=''): self.ingress_mac.update(data) return self.ingress_mac.digest() # frame-size: 3-byte integer size of frame, big endian encoded (excludes padding) # frame relates to body w/o padding w/o mac read_size = ceil16(body_size) if not len(data) >= read_size + 16: raise FormatError('insufficient body length') # FIXME check frame length in header # assume datalen == framelen for now frame_ciphertext = data[:read_size] frame_mac = data[read_size:read_size + 16] assert len(frame_mac) == 16 # ingres-mac.update(aes(mac-secret,ingres-mac) ^ # left128(ingres-mac.update(frame-ciphertext).digest)) fmac_seed = mac(frame_ciphertext) expected_frame_mac = mac(sxor(self.mac_enc(mac()[:16]), fmac_seed[:16]))[:16] if not frame_mac == expected_frame_mac: raise AuthenticationError('invalid frame mac') return aes(frame_ciphertext)[:body_size] def decrypt(self, data): header = self.decrypt_header(data[:32]) body_size = struct.unpack('>I', '\x00' + header[:3])[0] if not len(data) >= 32 + ceil16(body_size) + 16: raise FormatError('insufficient body length') frame = self.decrypt_body(data[32:], body_size) return dict(header=header, frame=frame, bytes_read=32 + ceil16(len(frame)) + 16) def create_auth_message(self, remote_pubkey, ephemeral_privkey=None, nonce=None): """ 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) """ assert self.is_initiator if not self.ecc.is_valid_key(remote_pubkey): raise InvalidKeyError('invalid remote pubkey') self.remote_pubkey = remote_pubkey token = self.token_by_pubkey.get(remote_pubkey) if not token: # new ecdh_shared_secret = self.ecc.get_ecdh_key(remote_pubkey) token = ecdh_shared_secret flag = 0x0 else: flag = 0x1 self.initiator_nonce = nonce or sha3( ienc(random.randint(0, 2**256 - 1))) assert len(self.initiator_nonce) == 32 token_xor_nonce = sxor(token, self.initiator_nonce) assert len(token_xor_nonce) == 32 ephemeral_pubkey = self.ephemeral_ecc.raw_pubkey assert len(ephemeral_pubkey) == 512 / 8 if not self.ecc.is_valid_key(ephemeral_pubkey): raise InvalidKeyError('invalid ephemeral pubkey') # S(ephemeral-privk, ecdh-shared-secret ^ nonce) S = self.ephemeral_ecc.sign(token_xor_nonce) assert len(S) == 65 # S || H(ephemeral-pubk) || pubk || nonce || 0x0 auth_message = S + sha3(ephemeral_pubkey) + self.ecc.raw_pubkey + \ self.initiator_nonce + chr(flag) assert len(auth_message) == 65 + 32 + 64 + 32 + 1 == 194 return auth_message def encrypt_auth_message(self, auth_message, remote_pubkey=None): assert self.is_initiator remote_pubkey = remote_pubkey or self.remote_pubkey self.auth_init = self.ecc.ecies_encrypt(auth_message, remote_pubkey) assert len(self.auth_init) == self.auth_message_ct_length return self.auth_init def encrypt_auth_ack_message(self, auth_message, remote_pubkey=None): assert not self.is_initiator remote_pubkey = remote_pubkey or self.remote_pubkey self.auth_ack = self.ecc.ecies_encrypt(auth_message, remote_pubkey) assert len(self.auth_ack) == self.auth_ack_message_ct_length return self.auth_ack def decode_authentication(self, ciphertext): """ 3. optionally, remote decrypts and verifies auth (checks that recovery of signature == H(ephemeral-pubk)) 4. 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) """ assert not self.is_initiator self.auth_init = ciphertext try: auth_message = self.ecc.ecies_decrypt(ciphertext) except RuntimeError as e: raise AuthenticationError(e) # S || H(ephemeral-pubk) || pubk || nonce || 0x[0|1] assert len( auth_message ) == 65 + 32 + 64 + 32 + 1 == 194 == self.auth_message_length signature = auth_message[:65] H_initiator_ephemeral_pubkey = auth_message[65:65 + 32] initiator_pubkey = auth_message[65 + 32:65 + 32 + 64] if not self.ecc.is_valid_key(initiator_pubkey): raise InvalidKeyError('invalid initiator pubkey') self.remote_pubkey = initiator_pubkey self.initiator_nonce = auth_message[65 + 32 + 64:65 + 32 + 64 + 32] known_flag = bool(ord(auth_message[65 + 32 + 64 + 32:])) # token or new ecdh_shared_secret if known_flag: self.remote_token_found = True # what todo if remote has token, but local forgot it. # reply with token not found. FIXME!!! token = self.token_by_pubkey.get(initiator_pubkey) assert token # FIXME continue session with ecdh_key and send flag in auth_ack else: token = self.ecc.get_ecdh_key(initiator_pubkey) # verify auth # S(ephemeral-privk, ecdh-shared-secret ^ nonce) signed = sxor(token, self.initiator_nonce) # recover initiator ephemeral pubkey self.remote_ephemeral_pubkey = ecdsa_recover(signed, signature) if not self.ecc.is_valid_key(self.remote_ephemeral_pubkey): raise InvalidKeyError('invalid remote ephemeral pubkey') if not ecdsa_verify(self.remote_ephemeral_pubkey, signature, signed): raise AuthenticationError('could not verify signature') # checks that recovery of signature == H(ephemeral-pubk) assert H_initiator_ephemeral_pubkey == sha3( self.remote_ephemeral_pubkey) def create_auth_ack_message(self, ephemeral_pubkey=None, nonce=None, token_found=False): """ authRecipient = E(remote-pubk, remote-ephemeral-pubk || nonce || 0x1) // token found authRecipient = E(remote-pubk, remote-ephemeral-pubk || nonce || 0x0) // token not found nonce and empehemeral-pubk are local! """ assert not self.is_initiator ephemeral_pubkey = ephemeral_pubkey or self.ephemeral_ecc.raw_pubkey self.responder_nonce = nonce or sha3( ienc(random.randint(0, 2**256 - 1))) flag = chr(1 if token_found else 0) msg = ephemeral_pubkey + self.responder_nonce + flag assert len(msg) == 64 + 32 + 1 == 97 == self.auth_ack_message_length return msg def decode_auth_ack_message(self, ciphertext): assert self.is_initiator self.auth_ack = ciphertext auth_ack_message = self.ecc.ecies_decrypt(ciphertext) assert len(auth_ack_message) == 64 + 32 + 1 self.remote_ephemeral_pubkey = auth_ack_message[:64] if not self.ecc.is_valid_key(self.remote_ephemeral_pubkey): raise InvalidKeyError('invalid remote ephemeral pubkey') self.responder_nonce = auth_ack_message[64:64 + 32] self.remote_token_found = bool(ord(auth_ack_message[-1])) def setup_cipher(self): # https://github.com/ethereum/cpp-ethereum/blob/develop/libp2p/RLPxFrameIO.cpp#L34 assert self.responder_nonce assert self.initiator_nonce assert self.auth_init assert self.auth_ack assert self.remote_ephemeral_pubkey if not self.ecc.is_valid_key(self.remote_ephemeral_pubkey): raise InvalidKeyError('invalid remote ephemeral pubkey') # derive base secrets from ephemeral key agreement # ecdhe-shared-secret = ecdh.agree(ephemeral-privkey, remote-ephemeral-pubk) ecdhe_shared_secret = self.ephemeral_ecc.get_ecdh_key( self.remote_ephemeral_pubkey) # shared-secret = sha3(ecdhe-shared-secret || sha3(nonce || initiator-nonce)) shared_secret = sha3(ecdhe_shared_secret + sha3(self.responder_nonce + self.initiator_nonce)) self.ecdhe_shared_secret = ecdhe_shared_secret # FIXME DEBUG self.shared_secret = shared_secret # FIXME DEBUG # token = sha3(shared-secret) self.token = sha3(shared_secret) self.token_by_pubkey[self.remote_pubkey] = self.token # aes-secret = sha3(ecdhe-shared-secret || shared-secret) self.aes_secret = sha3(ecdhe_shared_secret + shared_secret) # mac-secret = sha3(ecdhe-shared-secret || aes-secret) self.mac_secret = sha3(ecdhe_shared_secret + self.aes_secret) # setup sha3 instances for the MACs # egress-mac = sha3.update(mac-secret ^ recipient-nonce || auth-sent-init) mac1 = sha3_256( sxor(self.mac_secret, self.responder_nonce) + self.auth_init) # ingress-mac = sha3.update(mac-secret ^ initiator-nonce || auth-recvd-ack) mac2 = sha3_256( sxor(self.mac_secret, self.initiator_nonce) + self.auth_ack) if self.is_initiator: self.egress_mac, self.ingress_mac = mac1, mac2 else: self.egress_mac, self.ingress_mac = mac2, mac1 ciphername = 'aes-256-ctr' iv = "\x00" * 16 assert len(iv) == 16 self.aes_enc = pyelliptic.Cipher(self.aes_secret, iv, 1, ciphername=ciphername) self.aes_dec = pyelliptic.Cipher(self.aes_secret, iv, 0, ciphername=ciphername) self.mac_enc = AES.AESCipher(self.mac_secret, AES.MODE_ECB).encrypt self.is_ready = True
def test_handshake(): tv = test_values initiator = RLPxSession(ECCx(raw_privkey=tv['initiator_private_key']), is_initiator=True, ephemeral_privkey=tv['initiator_ephemeral_private_key']) initiator_pubkey = initiator.ecc.raw_pubkey responder = RLPxSession(ECCx(raw_privkey=tv['receiver_private_key']), ephemeral_privkey=tv['receiver_ephemeral_private_key']) responder_pubkey = responder.ecc.raw_pubkey # test encryption _enc = initiator.encrypt_auth_message(tv['auth_plaintext'], responder_pubkey) assert len(_enc) == len(tv['auth_ciphertext']) assert len(tv['auth_ciphertext']) == 113 + len(tv['auth_plaintext']) # len # test auth_msg plain auth_msg = initiator.create_auth_message(remote_pubkey=responder_pubkey, ephemeral_privkey=tv['initiator_ephemeral_private_key'], nonce=tv['initiator_nonce']) # test auth_msg plain assert len(auth_msg) == len(tv['auth_plaintext']) == 194 assert auth_msg[65:] == tv['auth_plaintext'][65:] # starts with non deterministic k _auth_msg_cipher = initiator.encrypt_auth_message(auth_msg, responder_pubkey) # test shared responder.ecc.get_ecdh_key(initiator_pubkey) == \ initiator.ecc.get_ecdh_key(responder_pubkey) # test decrypt assert auth_msg == responder.ecc.ecies_decrypt(_auth_msg_cipher) # check receive responder_ephemeral_pubkey = privtopub(tv['receiver_ephemeral_private_key']) auth_msg_cipher = tv['auth_ciphertext'] auth_msg = responder.ecc.ecies_decrypt(auth_msg_cipher) assert auth_msg[65:] == tv['auth_plaintext'][65:] # starts with non deterministic k responder.decode_authentication(auth_msg_cipher) auth_ack_msg = responder.create_auth_ack_message(responder_ephemeral_pubkey, nonce=tv['receiver_nonce']) assert auth_ack_msg == tv['authresp_plaintext'] auth_ack_msg_cipher = responder.encrypt_auth_ack_message(auth_ack_msg, remote_pubkey=responder.remote_pubkey) # set auth ack msg cipher (needed later for mac calculation) responder.auth_ack = tv['authresp_ciphertext'] responder.setup_cipher() assert responder.ecdhe_shared_secret == tv['ecdhe_shared_secret'] assert len(responder.token) == len(tv['token']) assert responder.token == tv['token'] assert responder.aes_secret == tv['aes_secret'] assert responder.mac_secret == tv['mac_secret'] assert responder.initiator_nonce == tv['initiator_nonce'] assert responder.responder_nonce == tv['receiver_nonce'] assert responder.auth_init == tv['auth_ciphertext'] assert responder.auth_ack == tv['authresp_ciphertext'] # test values are from initiator perspective? assert responder.ingress_mac.digest() == tv['initial_egress_MAC'] assert responder.ingress_mac.digest() == tv['initial_egress_MAC'] assert responder.egress_mac.digest() == tv['initial_ingress_MAC'] assert responder.egress_mac.digest() == tv['initial_ingress_MAC'] r = responder.decrypt(tv['initiator_hello_packet']) # unpack hello packet import struct import rlp import rlp.sedes as sedes from rlp.codec import consume_item header = r['header'] frame_length = struct.unpack(b'>I', b'\x00' + header[:3])[0] header_sedes = sedes.List([sedes.big_endian_int, sedes.big_endian_int]) header_data = rlp.decode(header[3:], strict=False, sedes=header_sedes) print('header', repr(header_data)) # frame frame = r['frame'] # normal: rlp(packet-type) [|| rlp(packet-data)] || padding packet_type, end = consume_item(frame, start=0) packet_type = rlp.decode(frame, sedes=sedes.big_endian_int, strict=False) print('packet_type', repr(packet_type)) # decode hello body _sedes_capabilites_tuple = sedes.List([sedes.binary, sedes.big_endian_int]) structure = [ ('version', sedes.big_endian_int), ('client_version_string', sedes.big_endian_int), ('capabilities', sedes.CountableList(_sedes_capabilites_tuple)), ('listen_port', sedes.big_endian_int), ('remote_pubkey', sedes.binary) ] hello_sedes = sedes.List([x[1] for x in structure]) frame_data = rlp.decode(frame[end:], sedes=hello_sedes) frame_data = dict((structure[i][0], x) for i, x in enumerate(frame_data)) print('frame', frame_data)
class RLPxSession(object): ephemeral_ecc = None remote_ephemeral_pubkey = None initiator_nonce = None responder_nonce = None auth_init = None auth_ack = None aes_secret = None token = None aes_enc = None aes_dec = None mac_enc = None egress_mac = None ingress_mac = None is_ready = False remote_token_found = False remote_pubkey = None auth_message_length = 194 auth_message_ct_length = auth_message_length + ECCx.ecies_encrypt_overhead_length auth_ack_message_length = 97 auth_ack_message_ct_length = auth_ack_message_length + ECCx.ecies_encrypt_overhead_length def __init__(self, ecc, is_initiator=False, token_by_pubkey=dict(), ephemeral_privkey=None): self.ecc = ecc self.is_initiator = is_initiator self.token_by_pubkey = token_by_pubkey self.ephemeral_ecc = ECCx(raw_privkey=ephemeral_privkey) def encrypt(self, header, frame): assert self.is_ready is True assert len(header) == 16 assert len(frame) % 16 == 0 def aes(data=''): return self.aes_enc.update(data) def mac(data=''): self.egress_mac.update(data) return self.egress_mac.digest() # header header_ciphertext = aes(header) assert len(header_ciphertext) == 16 # egress-mac.update(aes(mac-secret,egress-mac) ^ header-ciphertext).digest header_mac = mac(sxor(self.mac_enc(mac()[:16]), header_ciphertext))[:16] # frame frame_ciphertext = aes(frame) assert len(frame_ciphertext) == len(frame) # egress-mac.update(aes(mac-secret,egress-mac) ^ # left128(egress-mac.update(frame-ciphertext).digest)) fmac_seed = mac(frame_ciphertext) frame_mac = mac(sxor(self.mac_enc(mac()[:16]), fmac_seed[:16]))[:16] return header_ciphertext + header_mac + frame_ciphertext + frame_mac def decrypt_header(self, data): assert self.is_ready is True assert len(data) == 32 def aes(data=''): return self.aes_dec.update(data) def mac(data=''): self.ingress_mac.update(data) return self.ingress_mac.digest() header_ciphertext = data[:16] header_mac = data[16:32] # ingress-mac.update(aes(mac-secret,ingress-mac) ^ header-ciphertext).digest expected_header_mac = mac(sxor(self.mac_enc(mac()[:16]), header_ciphertext))[:16] # expected_header_mac = self.updateMAC(self.ingress_mac, header_ciphertext) if not expected_header_mac == header_mac: raise AuthenticationError('invalid header mac') return aes(header_ciphertext) def decrypt_body(self, data, body_size): assert self.is_ready is True def aes(data=''): return self.aes_dec.update(data) def mac(data=''): self.ingress_mac.update(data) return self.ingress_mac.digest() # frame-size: 3-byte integer size of frame, big endian encoded (excludes padding) # frame relates to body w/o padding w/o mac read_size = ceil16(body_size) if not len(data) >= read_size + 16: raise FormatError('insufficient body length') # FIXME check frame length in header # assume datalen == framelen for now frame_ciphertext = data[:read_size] frame_mac = data[read_size:read_size + 16] assert len(frame_mac) == 16 # ingres-mac.update(aes(mac-secret,ingres-mac) ^ # left128(ingres-mac.update(frame-ciphertext).digest)) fmac_seed = mac(frame_ciphertext) expected_frame_mac = mac(sxor(self.mac_enc(mac()[:16]), fmac_seed[:16]))[:16] if not frame_mac == expected_frame_mac: raise AuthenticationError('invalid frame mac') return aes(frame_ciphertext)[:body_size] def decrypt(self, data): header = self.decrypt_header(data[:32]) body_size = struct.unpack('>I', '\x00' + header[:3])[0] if not len(data) >= 32 + ceil16(body_size) + 16: raise FormatError('insufficient body length') frame = self.decrypt_body(data[32:], body_size) return dict(header=header, frame=frame, bytes_read=32 + ceil16(len(frame)) + 16) def create_auth_message(self, remote_pubkey, ephemeral_privkey=None, nonce=None): """ 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) """ assert self.is_initiator if not self.ecc.is_valid_key(remote_pubkey): raise InvalidKeyError('invalid remote pubkey') self.remote_pubkey = remote_pubkey token = self.token_by_pubkey.get(remote_pubkey) if not token: # new ecdh_shared_secret = self.ecc.get_ecdh_key(remote_pubkey) token = ecdh_shared_secret flag = 0x0 else: flag = 0x1 self.initiator_nonce = nonce or sha3(ienc(random.randint(0, 2 ** 256 - 1))) assert len(self.initiator_nonce) == 32 token_xor_nonce = sxor(token, self.initiator_nonce) assert len(token_xor_nonce) == 32 ephemeral_pubkey = self.ephemeral_ecc.raw_pubkey assert len(ephemeral_pubkey) == 512 / 8 if not self.ecc.is_valid_key(ephemeral_pubkey): raise InvalidKeyError('invalid ephemeral pubkey') # S(ephemeral-privk, ecdh-shared-secret ^ nonce) S = self.ephemeral_ecc.sign(token_xor_nonce) assert len(S) == 65 # S || H(ephemeral-pubk) || pubk || nonce || 0x0 auth_message = S + sha3(ephemeral_pubkey) + self.ecc.raw_pubkey + \ self.initiator_nonce + chr(flag) assert len(auth_message) == 65 + 32 + 64 + 32 + 1 == 194 return auth_message def encrypt_auth_message(self, auth_message, remote_pubkey=None): assert self.is_initiator remote_pubkey = remote_pubkey or self.remote_pubkey self.auth_init = self.ecc.ecies_encrypt(auth_message, remote_pubkey) assert len(self.auth_init) == self.auth_message_ct_length return self.auth_init def encrypt_auth_ack_message(self, auth_message, remote_pubkey=None): assert not self.is_initiator remote_pubkey = remote_pubkey or self.remote_pubkey self.auth_ack = self.ecc.ecies_encrypt(auth_message, remote_pubkey) assert len(self.auth_ack) == self.auth_ack_message_ct_length return self.auth_ack def decode_authentication(self, ciphertext): """ 3. optionally, remote decrypts and verifies auth (checks that recovery of signature == H(ephemeral-pubk)) 4. 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) """ assert not self.is_initiator self.auth_init = ciphertext try: auth_message = self.ecc.ecies_decrypt(ciphertext) except RuntimeError as e: raise AuthenticationError(e) # S || H(ephemeral-pubk) || pubk || nonce || 0x[0|1] assert len(auth_message) == 65 + 32 + 64 + 32 + 1 == 194 == self.auth_message_length signature = auth_message[:65] H_initiator_ephemeral_pubkey = auth_message[65:65 + 32] initiator_pubkey = auth_message[65 + 32:65 + 32 + 64] if not self.ecc.is_valid_key(initiator_pubkey): raise InvalidKeyError('invalid initiator pubkey') self.remote_pubkey = initiator_pubkey self.initiator_nonce = auth_message[65 + 32 + 64:65 + 32 + 64 + 32] known_flag = bool(ord(auth_message[65 + 32 + 64 + 32:])) # token or new ecdh_shared_secret if known_flag: self.remote_token_found = True # what todo if remote has token, but local forgot it. # reply with token not found. FIXME!!! token = self.token_by_pubkey.get(initiator_pubkey) assert token # FIXME continue session with ecdh_key and send flag in auth_ack else: token = self.ecc.get_ecdh_key(initiator_pubkey) # verify auth # S(ephemeral-privk, ecdh-shared-secret ^ nonce) signed = sxor(token, self.initiator_nonce) # recover initiator ephemeral pubkey self.remote_ephemeral_pubkey = ecdsa_recover(signed, signature) if not self.ecc.is_valid_key(self.remote_ephemeral_pubkey): raise InvalidKeyError('invalid remote ephemeral pubkey') if not ecdsa_verify(self.remote_ephemeral_pubkey, signature, signed): raise AuthenticationError('could not verify signature') # checks that recovery of signature == H(ephemeral-pubk) assert H_initiator_ephemeral_pubkey == sha3(self.remote_ephemeral_pubkey) def create_auth_ack_message(self, ephemeral_pubkey=None, nonce=None, token_found=False): """ authRecipient = E(remote-pubk, remote-ephemeral-pubk || nonce || 0x1) // token found authRecipient = E(remote-pubk, remote-ephemeral-pubk || nonce || 0x0) // token not found nonce and empehemeral-pubk are local! """ assert not self.is_initiator ephemeral_pubkey = ephemeral_pubkey or self.ephemeral_ecc.raw_pubkey self.responder_nonce = nonce or sha3(ienc(random.randint(0, 2 ** 256 - 1))) flag = chr(1 if token_found else 0) msg = ephemeral_pubkey + self.responder_nonce + flag assert len(msg) == 64 + 32 + 1 == 97 == self.auth_ack_message_length return msg def decode_auth_ack_message(self, ciphertext): assert self.is_initiator self.auth_ack = ciphertext auth_ack_message = self.ecc.ecies_decrypt(ciphertext) assert len(auth_ack_message) == 64 + 32 + 1 self.remote_ephemeral_pubkey = auth_ack_message[:64] if not self.ecc.is_valid_key(self.remote_ephemeral_pubkey): raise InvalidKeyError('invalid remote ephemeral pubkey') self.responder_nonce = auth_ack_message[64:64 + 32] self.remote_token_found = bool(ord(auth_ack_message[-1])) def setup_cipher(self): assert self.responder_nonce assert self.initiator_nonce assert self.auth_init assert self.auth_ack assert self.remote_ephemeral_pubkey if not self.ecc.is_valid_key(self.remote_ephemeral_pubkey): raise InvalidKeyError('invalid remote ephemeral pubkey') # derive base secrets from ephemeral key agreement # ecdhe-shared-secret = ecdh.agree(ephemeral-privkey, remote-ephemeral-pubk) ecdhe_shared_secret = self.ephemeral_ecc.get_ecdh_key(self.remote_ephemeral_pubkey) # shared-secret = sha3(ecdhe-shared-secret || sha3(nonce || initiator-nonce)) shared_secret = sha3( ecdhe_shared_secret + sha3(self.responder_nonce + self.initiator_nonce)) self.ecdhe_shared_secret = ecdhe_shared_secret # used in tests self.shared_secret = shared_secret # used in tests # token = sha3(shared-secret) self.token = sha3(shared_secret) self.token_by_pubkey[self.remote_pubkey] = self.token # aes-secret = sha3(ecdhe-shared-secret || shared-secret) self.aes_secret = sha3(ecdhe_shared_secret + shared_secret) # mac-secret = sha3(ecdhe-shared-secret || aes-secret) self.mac_secret = sha3(ecdhe_shared_secret + self.aes_secret) # setup sha3 instances for the MACs # egress-mac = sha3.update(mac-secret ^ recipient-nonce || auth-sent-init) mac1 = sha3_256(sxor(self.mac_secret, self.responder_nonce) + self.auth_init) # ingress-mac = sha3.update(mac-secret ^ initiator-nonce || auth-recvd-ack) mac2 = sha3_256(sxor(self.mac_secret, self.initiator_nonce) + self.auth_ack) if self.is_initiator: self.egress_mac, self.ingress_mac = mac1, mac2 else: self.egress_mac, self.ingress_mac = mac2, mac1 ciphername = 'aes-256-ctr' iv = "\x00" * 16 assert len(iv) == 16 self.aes_enc = pyelliptic.Cipher(self.aes_secret, iv, 1, ciphername=ciphername) self.aes_dec = pyelliptic.Cipher(self.aes_secret, iv, 0, ciphername=ciphername) self.mac_enc = AES.AESCipher(self.mac_secret, AES.MODE_ECB).encrypt self.is_ready = True
class RNOService(BaseService): # required by BaseService name = 'rno' default_config = dict(eth=dict(privkey_hex='')) # RNO address, where the requests for random number should be addressed to. my_addr = None # Keeps all transactions not yet processed by loop_body tx_queue = None # Will be used to a) sign transaction and b) encrypt random number using ECIES eccx = None privkey_hex = None def __init__(self, app): super(RNOService, self).__init__(app) log.info('Initializing RNO') self.config = app.config self.interrupt = Event() self.tx_queue = Queue() # thread safe self.privkey_hex = self.config['eth']['privkey_hex'].decode('hex') self.my_addr = privtoaddr(self.privkey_hex) self.eccx = ECCx(None, self.privkey_hex) # Process the transaction queue. There is no concurrency problem here since # the Queue is thread-safe. def loop_body(self): log.debug('RNO body', my_addr=self.my_addr) while not self.tx_queue.empty(): tx = self.tx_queue.get() if tx.to == self.my_addr: self.process_tx(tx) # Transactions should be added to a queue so that 'loop_body' process that queue # To minimize code dependency and coupling, this method will be called for ALL # transactions received. # It is called in the loop of eth_service.py -> on_receive_transactions def add_transaction(self, tx): log.debug('RNO received transaction', tx=tx) # All transactions are being queued here to minimize the blocking # of caller's thread. Transactions not addressed to RNO are discarded # in loop_body. self.tx_queue.put(tx) # This method is the core of the RNO. Transactions should NOT be processed in the # add_transaction otherwise it would block the caller. def process_tx(self, tx): log.debug('process tx', tx=tx) # 2) Extract sender's pubkey from the Electrum-style signature of the tx sender_pubkey = self.sender_pubkey_from_tx(tx) enc_num = self.generate_encrypted_random_number(sender_pubkey) # 5) encrypt RN using reveal host's pubkey (eRN2) (???) # this is not specified yet # 6) create/send transaction back to tx sender self.deliver(enc_num, tx.sender) # 7) create/send transaction to reveal host. # this is not specified yet def sender_pubkey_from_tx(self, tx): encoded_signature = _encode_sig(tx.v, tx.r, tx.s) message = None # TODO: find out how to build the data (message) where the signature is applied. return recover(message, encoded_signature) def generate_encrypted_random_number(self, pubkey): # 3) generate the random number number = os.urandom(64) # 4) encrypt RN using sender's pubkey (eRN1) return self.eccx.encrypt(number, pubkey) def deliver(self, enc_num, to): # nonce = number of transactions already sent by that account head = self.app.services.chain.chain.head nonce = head.get_nonce(self.my_addr) # Took from buterin example: # https://blog.ethereum.org/2014/04/10/pyethereum-and-serpent-programming-guide/ gasprice = 10**12 # Took from buterin example: # https://blog.ethereum.org/2014/04/10/pyethereum-and-serpent-programming-guide/ startgas = 10000 value = 0 # It's just a message, don't need to send any value (TODO: confirm that info) # data is a json formatted message but has to be 'binary' unix_now = int(round(time())) payload = {} payload['when'] = unix_now payload['number'] = enc_num payload['publish_on'] = unix_now + 86400 # in 24 hours payload['published_at'] = 'http://www.example.com/foo' data = json.dumps(payload) deliver_tx = Transaction(nonce, gasprice, startgas, to, value, data) signed_deliver_tx = deliver_tx.sign(self.privkey_hex) success, output = apply_transaction(head, signed_deliver_tx) # Sends the reply back to Requester and Reveal Host def send_replies(self, number, requester_addr, reveal_host_addr, publish_at, publish_on): log.debug('RNO reply', number=number, requester_addr=requester_addr, publish_at=publish_at, publish_on=publish_on) # Generates the public address (IPFS?) that will be used by the Reveal Host def generate_public_address(self, number): address = 'some public address' log.debug('RNO pub address', address=address) return address # This will make the loop_body be executed by RNOService thread. def wakeup(self): self.interrupt.set() # @override BaseService._run (Greenlet._run) def _run(self): while True: self.interrupt.wait() self.loop_body() self.interrupt.clear()
def __init__(self, privkey): self.ecc = ECCx(raw_privkey=privkey)
class RLPxSession(object): ephemeral_ecc = None remote_ephemeral_pubkey = None initiator_nonce = None responder_nonce = None auth_init = None auth_ack = None aes_secret = None token = None aes_enc = None aes_dec = None mac_enc = None egress_mac = None ingress_mac = None is_ready = False remote_pubkey = None remote_version = 0 got_eip8_auth, got_eip8_ack = False, False def __init__(self, ecc, is_initiator=False, ephemeral_privkey=None): self.ecc = ecc self.is_initiator = is_initiator self.ephemeral_ecc = ECCx(raw_privkey=ephemeral_privkey) ### frame handling def encrypt(self, header, frame): assert self.is_ready is True assert len(header) == 16 assert len(frame) % 16 == 0 def aes(data=''): return self.aes_enc.update(data) def mac(data=b''): data = str_to_bytes(data) self.egress_mac.update(data) return self.egress_mac.digest() # header header_ciphertext = aes(header) assert len(header_ciphertext) == 16 # egress-mac.update(aes(mac-secret,egress-mac) ^ header-ciphertext).digest header_mac = mac(sxor(self.mac_enc(mac()[:16]), header_ciphertext))[:16] # frame frame_ciphertext = aes(frame) assert len(frame_ciphertext) == len(frame) # egress-mac.update(aes(mac-secret,egress-mac) ^ # left128(egress-mac.update(frame-ciphertext).digest)) fmac_seed = mac(frame_ciphertext) frame_mac = mac(sxor(self.mac_enc(mac()[:16]), fmac_seed[:16]))[:16] return header_ciphertext + header_mac + frame_ciphertext + frame_mac def decrypt_header(self, data): assert self.is_ready is True assert len(data) == 32 def aes(data=''): return self.aes_dec.update(data) def mac(data=b''): data = str_to_bytes(data) self.ingress_mac.update(data) return self.ingress_mac.digest() header_ciphertext = data[:16] header_mac = data[16:32] # ingress-mac.update(aes(mac-secret,ingress-mac) ^ header-ciphertext).digest expected_header_mac = mac( sxor(self.mac_enc(mac()[:16]), header_ciphertext))[:16] # expected_header_mac = self.updateMAC(self.ingress_mac, header_ciphertext) if not expected_header_mac == header_mac: raise AuthenticationError('invalid header mac') return aes(header_ciphertext) def decrypt_body(self, data, body_size): assert self.is_ready is True def aes(data=''): return self.aes_dec.update(data) def mac(data=b''): data = str_to_bytes(data) self.ingress_mac.update(data) return self.ingress_mac.digest() # frame-size: 3-byte integer size of frame, big endian encoded (excludes padding) # frame relates to body w/o padding w/o mac read_size = ceil16(body_size) if not len(data) >= read_size + 16: raise FormatError('insufficient body length') # FIXME check frame length in header # assume datalen == framelen for now frame_ciphertext = data[:read_size] frame_mac = data[read_size:read_size + 16] assert len(frame_mac) == 16 # ingres-mac.update(aes(mac-secret,ingres-mac) ^ # left128(ingres-mac.update(frame-ciphertext).digest)) fmac_seed = mac(frame_ciphertext) expected_frame_mac = mac(sxor(self.mac_enc(mac()[:16]), fmac_seed[:16]))[:16] if not frame_mac == expected_frame_mac: raise AuthenticationError('invalid frame mac') return aes(frame_ciphertext)[:body_size] def decrypt(self, data): header = self.decrypt_header(data[:32]) body_size = struct.unpack(b'>I', b'\x00' + header[:3])[0] if not len(data) >= 32 + ceil16(body_size) + 16: raise FormatError('insufficient body length') frame = self.decrypt_body(data[32:], body_size) return dict(header=header, frame=frame, bytes_read=32 + ceil16(len(frame)) + 16) ### handshake auth message handling def create_auth_message(self, remote_pubkey, ephemeral_privkey=None, nonce=None): """ 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) """ assert self.is_initiator if not self.ecc.is_valid_key(remote_pubkey): raise InvalidKeyError('invalid remote pubkey') self.remote_pubkey = remote_pubkey ecdh_shared_secret = self.ecc.get_ecdh_key(remote_pubkey) token = ecdh_shared_secret flag = 0x0 self.initiator_nonce = nonce or sha3( ienc(random.randint(0, 2**256 - 1))) assert len(self.initiator_nonce) == 32 token_xor_nonce = sxor(token, self.initiator_nonce) assert len(token_xor_nonce) == 32 ephemeral_pubkey = self.ephemeral_ecc.raw_pubkey assert len(ephemeral_pubkey) == 512 / 8 if not self.ecc.is_valid_key(ephemeral_pubkey): raise InvalidKeyError('invalid ephemeral pubkey') # S(ephemeral-privk, ecdh-shared-secret ^ nonce) S = self.ephemeral_ecc.sign(token_xor_nonce) assert len(S) == 65 # S || H(ephemeral-pubk) || pubk || nonce || 0x0 auth_message = S + sha3(ephemeral_pubkey) + self.ecc.raw_pubkey + \ self.initiator_nonce + ascii_chr(flag) assert len(auth_message) == 65 + 32 + 64 + 32 + 1 == 194 return auth_message eip8_auth_sedes = sedes.List( [ sedes.Binary(min_length=65, max_length=65), # sig sedes.Binary(min_length=64, max_length=64), # pubkey sedes.Binary(min_length=32, max_length=32), # nonce sedes.BigEndianInt() # version ], strict=False) def encrypt_auth_message(self, auth_message, remote_pubkey=None): assert self.is_initiator remote_pubkey = remote_pubkey or self.remote_pubkey self.auth_init = self.ecc.ecies_encrypt(auth_message, remote_pubkey) assert len(self.auth_init) == 307 return self.auth_init def decode_authentication(self, ciphertext): """ 3. optionally, remote decrypts and verifies auth (checks that recovery of signature == H(ephemeral-pubk)) 4. 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) """ assert not self.is_initiator if len(ciphertext) < 307: raise FormatError("Ciphertext too short") try: (size, sig, initiator_pubkey, nonce, version) = self.decode_auth_plain(ciphertext) except AuthenticationError: (size, sig, initiator_pubkey, nonce, version) = self.decode_auth_eip8(ciphertext) self.got_eip8_auth = True self.auth_init = ciphertext[:size] # recover initiator ephemeral pubkey from sig # S(ephemeral-privk, ecdh-shared-secret ^ nonce) token = self.ecc.get_ecdh_key(initiator_pubkey) self.remote_ephemeral_pubkey = ecdsa_recover(sxor(token, nonce), sig) if not self.ecc.is_valid_key(self.remote_ephemeral_pubkey): raise InvalidKeyError('invalid remote ephemeral pubkey') self.initiator_nonce = nonce self.remote_pubkey = initiator_pubkey self.remote_version = version return ciphertext[size:] def decode_auth_plain(self, ciphertext): """ decode legacy pre-EIP-8 auth message format """ try: message = self.ecc.ecies_decrypt(ciphertext[:307]) except RuntimeError as e: raise AuthenticationError(e) assert len(message) == 194 signature = message[:65] pubkey = message[65 + 32:65 + 32 + 64] if not self.ecc.is_valid_key(pubkey): raise InvalidKeyError('invalid initiator pubkey') nonce = message[65 + 32 + 64:65 + 32 + 64 + 32] known_flag = bool(safe_ord(message[65 + 32 + 64 + 32:])) assert known_flag == 0 return (307, signature, pubkey, nonce, 4) def decode_auth_eip8(self, ciphertext): """ decode EIP-8 auth message format """ size = struct.unpack('>H', ciphertext[:2])[0] + 2 if len(ciphertext) < size: raise FormatError("Message shorter than specified size") try: message = self.ecc.ecies_decrypt(ciphertext[2:size], shared_mac_data=ciphertext[:2]) except RuntimeError as e: raise AuthenticationError(e) values = rlp.decode(message, sedes=self.eip8_auth_sedes, strict=False) assert len(values) >= 4 return (size, ) + values[:4] ### handshake ack message handling def create_auth_ack_message(self, version=supported_rlpx_version, eip8=False, ephemeral_pubkey=None, nonce=None): """ 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! """ assert not self.is_initiator ephemeral_pubkey = ephemeral_pubkey or self.ephemeral_ecc.raw_pubkey self.responder_nonce = nonce or sha3( ienc(random.randint(0, 2**256 - 1))) if eip8 or self.got_eip8_auth: msg = self.create_eip8_auth_ack_message(ephemeral_pubkey, self.responder_nonce, version) assert len(msg) > 97 else: msg = ephemeral_pubkey + self.responder_nonce + b'\x00' assert len(msg) == 97 return msg eip8_ack_sedes = sedes.List( [ sedes.Binary(min_length=64, max_length=64), # ephemeral pubkey sedes.Binary(min_length=32, max_length=32), # nonce sedes.BigEndianInt() # version ], strict=False) def create_eip8_auth_ack_message(self, ephemeral_pubkey, nonce, version): data = rlp.encode((ephemeral_pubkey, nonce, version), sedes=self.eip8_ack_sedes) pad = os.urandom(random.randint(100, 250)) return data + pad def encrypt_auth_ack_message(self, ack_message, eip8=False, remote_pubkey=None): assert not self.is_initiator remote_pubkey = remote_pubkey or self.remote_pubkey if eip8 or self.got_eip8_auth: # The EIP-8 version has an authenticated length prefix. prefix = struct.pack( '>H', len(ack_message) + self.ecc.ecies_encrypt_overhead_length) self.auth_ack = self.ecc.ecies_encrypt(ack_message, remote_pubkey, shared_mac_data=prefix) self.auth_ack = prefix + self.auth_ack else: self.auth_ack = self.ecc.ecies_encrypt(ack_message, remote_pubkey) assert len(self.auth_ack) == 210 return self.auth_ack def decode_auth_ack_message(self, ciphertext): assert self.is_initiator assert len(ciphertext) >= 210 try: (size, eph_pubkey, nonce, version) = self.decode_ack_plain(ciphertext) except AuthenticationError: (size, eph_pubkey, nonce, version) = self.decode_ack_eip8(ciphertext) self.got_eip8_ack = True self.auth_ack = ciphertext[:size] self.remote_ephemeral_pubkey = eph_pubkey[:64] self.responder_nonce = nonce self.remote_version = version if not self.ecc.is_valid_key(self.remote_ephemeral_pubkey): raise InvalidKeyError('invalid remote ephemeral pubkey') return ciphertext[size:] def decode_ack_plain(self, ciphertext): """ decode legacy pre-EIP-8 ack message format """ try: message = self.ecc.ecies_decrypt(ciphertext[:210]) except RuntimeError as e: raise AuthenticationError(e) assert len(message) == 64 + 32 + 1 eph_pubkey = message[:64] nonce = message[64:64 + 32] known = safe_ord(message[-1]) assert known == 0 return (210, eph_pubkey, nonce, 4) def decode_ack_eip8(self, ciphertext): """ decode EIP-8 ack message format """ size = struct.unpack('>H', ciphertext[:2])[0] + 2 assert len(ciphertext) == size try: message = self.ecc.ecies_decrypt(ciphertext[2:size], shared_mac_data=ciphertext[:2]) except RuntimeError as e: raise AuthenticationError(e) values = rlp.decode(message, sedes=self.eip8_ack_sedes, strict=False) assert len(values) >= 3 return (size, ) + values[:3] ### handshake key derivation def setup_cipher(self): assert self.responder_nonce assert self.initiator_nonce assert self.auth_init assert self.auth_ack assert self.remote_ephemeral_pubkey if not self.ecc.is_valid_key(self.remote_ephemeral_pubkey): raise InvalidKeyError('invalid remote ephemeral pubkey') # derive base secrets from ephemeral key agreement # ecdhe-shared-secret = ecdh.agree(ephemeral-privkey, remote-ephemeral-pubk) ecdhe_shared_secret = self.ephemeral_ecc.get_ecdh_key( self.remote_ephemeral_pubkey) # shared-secret = sha3(ecdhe-shared-secret || sha3(nonce || initiator-nonce)) shared_secret = sha3(ecdhe_shared_secret + sha3(self.responder_nonce + self.initiator_nonce)) self.ecdhe_shared_secret = ecdhe_shared_secret # used in tests self.shared_secret = shared_secret # used in tests # token = sha3(shared-secret) self.token = sha3(shared_secret) # aes-secret = sha3(ecdhe-shared-secret || shared-secret) self.aes_secret = sha3(ecdhe_shared_secret + shared_secret) # mac-secret = sha3(ecdhe-shared-secret || aes-secret) self.mac_secret = sha3(ecdhe_shared_secret + self.aes_secret) # setup sha3 instances for the MACs # egress-mac = sha3.update(mac-secret ^ recipient-nonce || auth-sent-init) mac1 = sha3_256( sxor(self.mac_secret, self.responder_nonce) + self.auth_init) # ingress-mac = sha3.update(mac-secret ^ initiator-nonce || auth-recvd-ack) mac2 = sha3_256( sxor(self.mac_secret, self.initiator_nonce) + self.auth_ack) if self.is_initiator: self.egress_mac, self.ingress_mac = mac1, mac2 else: self.egress_mac, self.ingress_mac = mac2, mac1 ciphername = 'aes-256-ctr' iv = "\x00" * 16 assert len(iv) == 16 self.aes_enc = pyelliptic.Cipher(self.aes_secret, iv, 1, ciphername=ciphername) self.aes_dec = pyelliptic.Cipher(self.aes_secret, iv, 0, ciphername=ciphername) self.mac_enc = AES.new(self.mac_secret, AES.MODE_ECB).encrypt self.is_ready = True
class RLPxSession(object): ephemeral_ecc = None nonce = None token = None aes_secret = None aes_enc = None aes_dec = None egress_mac = None ingress_mac = None remote_node = None _authentication_sent = False is_ready = False def __init__(self, peer=None): # persisted peer data. keys are the nodeid # session data self.peer = peer if peer: self.node = peer.local_node.ecc else: self.node = None def __repr__(self): return '<RLPxSession (%s)>' % self.address.encode('hex') def encrypt(self, header, frame): """ header-mac: right128 of egress-mac.update(aes(mac-secret,egress-mac)^header-ciphertext) """ assert self.is_ready is True def aes(data): return self.aes_enc.update(data) def mac(data): return self.egress_mac.update(data) # header assert len(header) == 16 # zero padded to 16 bytes header_ciphertext = aes(header) assert len(header_ciphertext) <= 32 # must not be larger than mac # FIXME mac-secret!? header_mac = mac(sxor(aes(mac('')), header_ciphertext))[-16:] # frame frame_ciphertext = aes(frame) frame_mac = self.egress_mac.update(frame_ciphertext) return header_ciphertext + header_mac + frame_ciphertext + frame_mac def decrypt(self, data): assert self.is_ready is True def aes(data): return self.aes_dec.update(data) def mac(data): return self.egress_mac.update(data) header_ciphertext = data[:16] header_mac = data[16:32] header = aes(header_ciphertext) expected_header_mac = mac(sxor(aes(mac(''), header_ciphertext)))[-16:] assert expected_header_mac == header_mac # FIXME check frame length in header # assume datalen == framelen for now frame_mac = self.egress_mac.update(frame_ciphertext) data = aes(data[32:]) def create_auth_message(self, remote_pubkey, token=None, ephemeral_privkey=None, nonce=None): """ 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) """ if not token: # new ecdh_shared_secret = self.node.get_ecdh_key(remote_pubkey) token = ecdh_shared_secret flag = 0x0 else: flag = 0x1 nonce = nonce or ienc(random.randint(0, 2**256 - 1)) assert len(nonce) == 32 token_xor_nonce = sxor(token, nonce) assert len(token_xor_nonce) == 32 # generate session ephemeral key if not ephemeral_privkey: ephemeral_privkey = sha3(ienc(random.randint(0, 2**256 - 1))) self.ephemeral_ecc = ECCx(raw_privkey=ephemeral_privkey) ephemeral_pubkey = self.ephemeral_ecc.raw_pubkey assert len(ephemeral_pubkey) == 512 / 8 # S(ephemeral-privk, ecdh-shared-secret ^ nonce) S = self.ephemeral_ecc.sign(token_xor_nonce) assert len(S) == 65 # S || H(ephemeral-pubk) || pubk || nonce || 0x0 auth_message = S + sha3( ephemeral_pubkey) + self.node.raw_pubkey + nonce + chr(flag) assert len(auth_message) == 65 + 32 + 64 + 32 + 1 == 194 return auth_message def encrypt_auth_message(self, auth_message, remote_pubkey): return self.node.ecies_encrypt(auth_message, remote_pubkey) encrypt_auth_ack_message = encrypt_auth_message def send_authentication(self, remote_node, ephermal_privkey=None): auth_message = self.create_auth_message(remote_node, ephermal_privkey) self.peer.send(auth_message) self._authentication_sent = True def receive_authentication(self, ciphertext): """ 3. optionally, remote decrypts and verifies auth (checks that recovery of signature == H(ephemeral-pubk)) 4. 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) """ auth_message = self.node.ecies_decrypt(ciphertext) # S || H(ephemeral-pubk) || pubk || nonce || 0x[0|1] assert len(auth_message) == 65 + 32 + 64 + 32 + 1 == 194 signature = auth_message[:65] H_remote_ephemeral_pubkey = auth_message[65:65 + 32] remote_pubkey = auth_message[65 + 32:65 + 32 + 64] nonce = auth_message[65 + 32 + 64:65 + 32 + 64 + 32] known_flag = auth_message[65 + 32 + 64 + 32:] # token or new ecdh_shared_secret token_database = dict() # FIXME token_found = False if known_flag == 1: token = token_database.get(remote_pubkey) if token: token_found = True else: token = ecdh_shared_secret = self.node.get_ecdh_key(remote_pubkey) # verify auth # S(ephemeral-privk, ecdh-shared-secret ^ nonce) ecdh_shared_secret = self.node.get_ecdh_key(remote_pubkey) signed = sxor(ecdh_shared_secret, nonce) # recover remote ephemeral pubkey remote_ephemeral_pubkey = ecdsa_recover(signed, signature) assert ecdsa_verify(remote_ephemeral_pubkey, signature, signed) # checks that recovery of signature == H(ephemeral-pubk) assert H_remote_ephemeral_pubkey == sha3(remote_ephemeral_pubkey) return dict(remote_ephemeral_pubkey=remote_ephemeral_pubkey, token=token, token_found=token_found, ecdh_shared_secret=ecdh_shared_secret, remote_pubkey=remote_pubkey, nonce=nonce, known_flag=known_flag) def create_auth_ack_message(self, ephemeral_pubkey, nonce, token_found=False): """ authRecipient = E(remote-pubk, remote-ephemeral-pubk || nonce || 0x1) // token found authRecipient = E(remote-pubk, remote-ephemeral-pubk || nonce || 0x0) // token not found nonce and empehemeral-pubk are local! """ flag = chr(1 if token_found else 0) msg = ephemeral_pubkey + nonce + flag assert len(msg) == 64 + 32 + 1 == 97 return msg def something(): ################## # send authentication if not yet if not self._authentication_sent: remote_node = RemoteNode(remote_pubkey) # FIXME LOOKUP self.send_authentication(remote_node) # - success -> AcknowledgeAuthentication self.acknowledge_authentication(other, remote_pubkey, remote_ecdhe_pubkey) # ecdhe_shared_secret = ecdh.agree(ecdhe-random, ecdhe-random-public) # Compute public key with the local private key and return a 512bits shared key ecdhe_shared_secret = self.ephemeral_ecc.get_ecdh_key(remote_pubkey) ecdhe_pubkey = self.ephemeral_ecc.get_pubkey() # shared-secret = sha3(ecdhe-shared-secret || sha3(nonce || remote-nonce)) shared_secret = sha3(ecdhe_shared_secret + sha3(ienc(self.nonce) + ienc(remote_nonce))) self.aes_secret = sha3(ecdhe_shared_secret + shared_secret) self.mac_secret = sha3(ecdhe_shared_secret + self.aes_secret) # egress-mac = sha3(mac-secret^nonce || auth) self.egress_mac = sha3(sxor(self.mac_secret, self.nonce) + ciphertext) # ingress-mac = sha3(mac-secret^remote-nonce || auth) self.ingress_mac = sha3( sxor(self.mac_secret, remote_nonce) + ciphertext) self.token = sha3(shared_secret) iv = pyelliptic.Cipher.gen_IV('aes-256-ctr') self.aes_enc = pyelliptic.Cipher(self.aes_secret, iv, 1, ciphername='aes-256-ctr') self.aes_dec = pyelliptic.Cipher(self.aes_secret, iv, 0, ciphername='aes-256-ctr') self.is_ready = True
def __init__(self, ecc, is_initiator=False, ephemeral_privkey=None): self.ecc = ecc self.is_initiator = is_initiator self.ephemeral_ecc = ECCx(raw_privkey=ephemeral_privkey)
def test_ecies_decrypt(): tv = test_values e = ECCx(raw_privkey=tv['receiver_private_key']) _dec = e.ecies_decrypt(tv['auth_ciphertext']) assert len(_dec) == len(tv['auth_plaintext']) assert _dec == tv['auth_plaintext']