def create_eip8_auth_ack_message(self, ephemeral_pubkey, nonce, version): eip8_ack_serializers = sedes.List( [ sedes.Binary(min_length=eth_common_constants.PUBLIC_KEY_LEN, max_length=eth_common_constants.PUBLIC_KEY_LEN ), # ephemeral pubkey sedes.Binary( min_length=eth_common_constants.AUTH_NONCE_LEN, max_length=eth_common_constants.AUTH_NONCE_LEN), # nonce sedes.BigEndianInt() # version ], strict=False) data = rlp.encode((ephemeral_pubkey, nonce, version), sedes=eip8_ack_serializers) pad = os.urandom( random.randint(eth_common_constants.EIP8_ACK_PAD_MIN, eth_common_constants.EIP8_ACK_PAD_MAX)) return data + pad
def parse_eip8_auth_message(self, message): eip8_auth_serializers = sedes.List( [ sedes.Binary( min_length=eth_common_constants.SIGNATURE_LEN, max_length=eth_common_constants.SIGNATURE_LEN), # sig sedes.Binary(min_length=eth_common_constants.PUBLIC_KEY_LEN, max_length=eth_common_constants.PUBLIC_KEY_LEN), # pubkey sedes.Binary( min_length=eth_common_constants.AUTH_NONCE_LEN, max_length=eth_common_constants.AUTH_NONCE_LEN), # nonce sedes.BigEndianInt() # version ], strict=False) values = rlp.decode(message, sedes=eip8_auth_serializers, strict=False) signature, pubkey, nonce, version = values return signature, pubkey, nonce, version
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
# Unused, according to EIP-8, but must be included nevertheless. token_flag = b"\x00" msg = self.ephemeral_pubkey.to_bytes() + nonce + token_flag return msg def encrypt_auth_ack_message(self, ack_message: bytes) -> bytes: if self.use_eip8: auth_ack = encrypt_eip8_msg(ack_message, self.remote.pubkey) else: auth_ack = ecies.encrypt(ack_message, self.remote.pubkey) return auth_ack 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, ) 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, )
from rlp import sedes from eth.rlp.headers import BlockHeader from eth.rlp.receipts import Receipt from eth.rlp.transactions import BaseTransactionFields from p2p.protocol import Command from trinity.rlp.block_body import BlockBody from trinity.rlp.sedes import HashOrNumber hash_sedes = sedes.Binary(min_length=32, max_length=32) class Status(Command): _cmd_id = 0 structure = ( ('protocol_version', sedes.big_endian_int), ('network_id', sedes.big_endian_int), ('td', sedes.big_endian_int), ('best_hash', hash_sedes), ('genesis_hash', hash_sedes), ) class NewBlockHashes(Command): _cmd_id = 1 structure = sedes.CountableList( sedes.List([hash_sedes, sedes.big_endian_int]))
class BlockHeader(rlp.Serializable): # type: ignore fields = [ ('parent_hash', hash32), ('uncles_hash', hash32), ('coinbase', address), ('state_root', trie_root), ('transaction_root', trie_root), ('receipt_root', trie_root), ('bloom', uint256), ('difficulty', sedes.big_endian_int), ('block_number', sedes.big_endian_int), ('gas_limit', sedes.big_endian_int), ('gas_used', sedes.big_endian_int), ('timestamp', sedes.big_endian_int), ('extra_data', sedes.binary), ('mix_hash', sedes.binary), ('nonce', sedes.Binary(8, allow_empty=True)) ] def __init__(self, difficulty: int, block_number: BlockNumber, gas_limit: int, timestamp: int, coinbase: Address, parent_hash: Hash32, uncles_hash: Hash32, state_root: Hash32, transaction_root: Hash32, receipt_root: Hash32, bloom: int, gas_used: int, extra_data: bytes, mix_hash: Hash32, nonce: bytes) -> None: super().__init__( parent_hash=parent_hash, uncles_hash=uncles_hash, coinbase=coinbase, state_root=state_root, transaction_root=transaction_root, receipt_root=receipt_root, bloom=bloom, difficulty=difficulty, block_number=block_number, gas_limit=gas_limit, gas_used=gas_used, timestamp=timestamp, extra_data=extra_data, mix_hash=mix_hash, nonce=nonce, ) def __str__(self) -> str: return '<BlockHeader #{0} {1}>'.format( self.block_number, humanize_hash(self.hash), ) _hash: Optional[Hash32] = None @property def hash(self) -> Hash32: if self._hash is None: self._hash = Hash32(keccak(rlp.encode(self))) return self._hash @property def hex_hash(self) -> str: return encode_hex(self.hash)