def test_incorrect_public_key_length_raises_critical_error(self): sk = X448PrivateKey.generate() for key in [ key_len * b'a' for key_len in range(1, 100) if key_len != TFC_PUBLIC_KEY_LENGTH ]: with self.assertRaises(SystemExit): X448.shared_key(sk, key)
def test_deriving_shared_secret_with_an_invalid_size_public_key_raises_critical_error( self) -> None: private_key = X448.generate_private_key() invalid_public_keys = [ key_length * b'a' for key_length in (1, TFC_PUBLIC_KEY_LENGTH - 1, TFC_PUBLIC_KEY_LENGTH + 1, 1000) ] for invalid_public_key in invalid_public_keys: with self.assertRaises(SystemExit): X448.shared_key(private_key, invalid_public_key)
def test_x448_with_the_official_test_vectors(self) -> None: sk_alice_ = X448PrivateKey.from_private_bytes(TestX448.sk_alice) sk_bob_ = X448PrivateKey.from_private_bytes(TestX448.sk_bob) self.assertEqual(X448.derive_public_key(sk_alice_), TestX448.pk_alice) self.assertEqual(X448.derive_public_key(sk_bob_), TestX448.pk_bob) shared_secret1 = X448.shared_key(sk_alice_, TestX448.pk_bob) shared_secret2 = X448.shared_key(sk_bob_, TestX448.pk_alice) self.assertEqual(shared_secret1, blake2b(TestX448.shared_secret)) self.assertEqual(shared_secret2, blake2b(TestX448.shared_secret))
def update_url_token(url_token_private_key: 'X448PrivateKey', ut_pubkey_hex: str, cached_pk: str, onion_pub_key: bytes, queues: 'QueueDict' ) -> Tuple[str, str]: """Update URL token for contact. When contact's URL token public key changes, update URL token. """ if ut_pubkey_hex == cached_pk: raise SoftError("URL token public key has not changed.", output=False) try: public_key = bytes.fromhex(ut_pubkey_hex) if len(public_key) != TFC_PUBLIC_KEY_LENGTH or public_key == bytes(TFC_PUBLIC_KEY_LENGTH): raise ValueError url_token = X448.shared_key(url_token_private_key, public_key).hex() queues[URL_TOKEN_QUEUE].put((onion_pub_key, url_token)) # Update Flask server's URL token for contact return url_token, ut_pubkey_hex except (TypeError, ValueError): raise SoftError("URL token derivation failed.", output=False)
def test_deriving_zero_shared_secret_raises_critical_error(self) -> None: """\ Some experts such as JP Aumasson[1] and Thai Duong[2] have argued that X25519 public keys should be validated before use to prevent one party from having key control, i.e., being able to force the shared secret to a preselected value. This also applies to X448. It's not clear how this type of attack could be leveraged in the context of secure messaging where both the sender and the recipient desire confidentiality, and where easier ways to break the confidentiality of the conversation exist for both parties. However, there is a) no harm in doing the check and b) no need to trouble ourselves with whether TFC should ensure contributory behavior; the pyca/cryptography library already checks that the shared secret is not zero. This test merely verifies that the check takes place. [1] https://research.kudelskisecurity.com/2017/04/25/should-ecdh-keys-be-validated/ [2] https://vnhacker.blogspot.com/2015/09/why-not-validating-curve25519-public.html """ with self.assertRaises(SystemExit): X448.shared_key(X448.generate_private_key(), bytes(TFC_PUBLIC_KEY_LENGTH))
def start_key_exchange( onion_pub_key: bytes, # Public key of contact's v3 Onion Service nick: str, # Contact's nickname contact_list: 'ContactList', # ContactList object settings: 'Settings', # Settings object queues: 'QueueDict' # Dictionary of multiprocessing queues ) -> None: """Start X448 key exchange with the recipient. This function first creates the X448 key pair. It then outputs the public key to Relay Program on Networked Computer, that passes the public key to contact's Relay Program where it is displayed. When the contact's public key reaches the user's Relay Program, the user will manually type the key into their Transmitter Program. The X448 shared secret is used to create unidirectional message and header keys, that will be used in forward secret communication. This is followed by the fingerprint verification where the user manually authenticates the public key. Once the fingerprint has been accepted, this function will add the contact/key data to contact/key databases, and export that data to the Receiver Program on Destination Computer. The transmission is encrypted with the local key. --- TFC provides proactive security by making fingerprint verification part of the key exchange. This prevents the situation where the users don't know about the feature, and thus helps minimize the risk of MITM attack. The fingerprints can be skipped by pressing Ctrl+C. This feature is not advertised however, because verifying fingerprints the only strong way to be sure TFC is not under MITM attack. When verification is skipped, TFC marks the contact's X448 keys as "Unverified". The fingerprints can later be verified with the `/verify` command: answering `yes` to the question on whether the fingerprints match, marks the X448 keys as "Verified". Variable naming: tx = user's key rx = contact's key fp = fingerprint mk = message key hk = header key """ if not contact_list.has_pub_key(onion_pub_key): contact_list.add_contact(onion_pub_key, nick, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), KEX_STATUS_PENDING, settings.log_messages_by_default, settings.accept_files_by_default, settings.show_notifications_by_default) contact = contact_list.get_contact_by_pub_key(onion_pub_key) # Generate new private key or load cached private key if contact.tfc_private_key is None: tfc_private_key_user = X448.generate_private_key() else: tfc_private_key_user = contact.tfc_private_key try: tfc_public_key_user = X448.derive_public_key(tfc_private_key_user) kdk_hash = contact_list.get_contact_by_pub_key( LOCAL_PUBKEY).tx_fingerprint tfc_public_key_contact = exchange_public_keys(onion_pub_key, tfc_public_key_user, kdk_hash, contact, settings, queues) validate_contact_public_key(tfc_public_key_contact) dh_shared_key = X448.shared_key(tfc_private_key_user, tfc_public_key_contact) tx_mk, rx_mk, tx_hk, rx_hk, tx_fp, rx_fp \ = X448.derive_subkeys(dh_shared_key, tfc_public_key_user, tfc_public_key_contact) kex_status = validate_contact_fingerprint(tx_fp, rx_fp) deliver_contact_data(KEY_EX_ECDHE, nick, onion_pub_key, tx_mk, rx_mk, tx_hk, rx_hk, queues, settings) # Store contact data into databases contact.tfc_private_key = None contact.tx_fingerprint = tx_fp contact.rx_fingerprint = rx_fp contact.kex_status = kex_status contact_list.store_contacts() queues[KEY_MANAGEMENT_QUEUE].put( (KDB_ADD_ENTRY_HEADER, onion_pub_key, tx_mk, csprng(), tx_hk, csprng())) m_print(f"Successfully added {nick}.", bold=True, tail_clear=True, delay=1, head=1) except (EOFError, KeyboardInterrupt): contact.tfc_private_key = tfc_private_key_user raise SoftError("Key exchange interrupted.", tail_clear=True, delay=1, head=2)
class TestClient(unittest.TestCase): url_token_private_key = X448.generate_private_key() url_token_public_key = X448.derive_public_key(url_token_private_key) url_token = X448.shared_key(url_token_private_key, url_token_public_key).hex() class MockResponse(object): """Mock Response object.""" def __init__(self, text): """Create new MockResponse object.""" self.text = text self.content = text class MockSession(object): """Mock Session object.""" def __init__(self): """Create new MockSession object.""" self.proxies = dict() self.timeout = None self.url = None self.test_no = 0 def get(self, url, timeout=0, stream=False): """Mock .get() method.""" self.timeout = timeout # When we reach `get_data_loop` that loads stream, throw exception to close the test. if stream: (_ for _ in ()).throw(requests.exceptions.RequestException) if url.startswith( "http://hpcrayuxhrcy2wtpfwgwjibderrvjll6azfr4tqat3eka2m2gbb55bid.onion/" ): if self.test_no == 0: self.test_no += 1 (_ for _ in ()).throw(requests.exceptions.RequestException) if self.test_no == 1: self.test_no += 1 return TestClient.MockResponse('OK') # Test function recovers from RequestException. if self.test_no == 2: self.test_no += 1 (_ for _ in ()).throw(requests.exceptions.RequestException) # Test function recovers from invalid public key. if self.test_no == 3: self.test_no += 1 return TestClient.MockResponse( ((ONION_SERVICE_PUBLIC_KEY_LENGTH - 1) * b'a').hex()) # Test client prints online/offline messages. elif self.test_no < 10: self.test_no += 1 return TestClient.MockResponse('') # Test valid public key moves function to `get_data_loop`. elif self.test_no == 10: self.test_no += 1 return TestClient.MockResponse( TestClient.url_token_public_key.hex()) @staticmethod def mock_session(): """Return MockSession object.""" return TestClient.MockSession() def setUp(self): self.o_session = requests.session self.queues = gen_queue_dict() requests.session = TestClient.mock_session def tearDown(self): requests.session = self.o_session tear_queues(self.queues) @mock.patch('time.sleep', return_value=None) def test_client(self, _): onion_pub_key = nick_to_pub_key('Alice') onion_address = nick_to_onion_address('Alice') tor_port = '1337' settings = Gateway() sk = TestClient.url_token_private_key self.assertIsNone( client(onion_pub_key, self.queues, sk, tor_port, settings, onion_address, unittest=True)) self.assertEqual(self.queues[URL_TOKEN_QUEUE].get(), (onion_pub_key, TestClient.url_token))
class TestGetDataLoop(unittest.TestCase): url_token_private_key_user = X448.generate_private_key() url_token_public_key_user = X448.derive_public_key( url_token_private_key_user) url_token_public_key_contact = X448.derive_public_key( X448.generate_private_key()) url_token = X448.shared_key(url_token_private_key_user, url_token_public_key_contact).hex() class MockResponse(object): """Mock Response object.""" def __init__(self): self.test_no = 0 def iter_lines(self): """Return data depending test number.""" self.test_no += 1 message = b'' # Empty message if self.test_no == 1: pass # Invalid message elif self.test_no == 2: message = MESSAGE_DATAGRAM_HEADER + b'\x1f' # Valid message elif self.test_no == 3: message = MESSAGE_DATAGRAM_HEADER + base64.b85encode( b'test') + b'\n' # Invalid public key elif self.test_no == 4: message = PUBLIC_KEY_DATAGRAM_HEADER + base64.b85encode( (TFC_PUBLIC_KEY_LENGTH - 1) * b'\x01') # Valid public key elif self.test_no == 5: message = PUBLIC_KEY_DATAGRAM_HEADER + base64.b85encode( TFC_PUBLIC_KEY_LENGTH * b'\x01') # Group management headers elif self.test_no == 6: message = GROUP_MSG_INVITE_HEADER elif self.test_no == 7: message = GROUP_MSG_JOIN_HEADER elif self.test_no == 8: message = GROUP_MSG_MEMBER_ADD_HEADER elif self.test_no == 9: message = GROUP_MSG_MEMBER_REM_HEADER elif self.test_no == 10: message = GROUP_MSG_EXIT_GROUP_HEADER # Invalid header elif self.test_no == 11: message = b'\x1f' # RequestException (no remaining data) elif self.test_no == 12: (_ for _ in ()).throw(requests.exceptions.RequestException) return message.split(b'\n') class MockFileResponse(object): """MockFileResponse object.""" def __init__(self, content): self.content = content class Session(object): """Mock session object.""" def __init__(self) -> None: """Create new Session object.""" self.proxies = dict() self.timeout = None self.url = None self.stream = False self.test_no = 0 self.response = TestGetDataLoop.MockResponse() self.url_token = TestGetDataLoop.url_token self.onion_url = 'http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion' def get(self, url: str, timeout: int = 0, stream: bool = False) -> Any: """Return data depending on what test is in question.""" self.stream = stream self.timeout = timeout if url == f"{self.onion_url}/{self.url_token}/messages": # Test function recovers from RequestException. if self.test_no == 1: self.test_no += 1 (_ for _ in ()).throw(requests.exceptions.RequestException) if self.test_no >= 2: self.test_no += 1 return self.response elif url == f"{self.onion_url}/{self.url_token}/files": # Test file data is received if self.test_no == 0: self.test_no += 1 return TestGetDataLoop.MockFileResponse(b'test') # Test function recovers from RequestException. if self.test_no > 1: (_ for _ in ()).throw(requests.exceptions.RequestException) @staticmethod def mock_session() -> Session: """Return mock Session object.""" return TestGetDataLoop.Session() def setUp(self): self.o_session = requests.session self.queues = gen_queue_dict() requests.session = TestGetDataLoop.mock_session def tearDown(self): requests.session = self.o_session tear_queues(self.queues) def test_get_data_loop(self): onion_pub_key = bytes(ONION_SERVICE_PUBLIC_KEY_LENGTH) settings = Gateway() onion_addr = pub_key_to_onion_address( bytes(ONION_SERVICE_PUBLIC_KEY_LENGTH)) short_addr = pub_key_to_short_address( bytes(ONION_SERVICE_PUBLIC_KEY_LENGTH)) url_token = TestGetDataLoop.url_token session = TestGetDataLoop.mock_session() self.assertIsNone( get_data_loop(onion_addr, url_token, short_addr, onion_pub_key, self.queues, session, settings)) self.assertIsNone( get_data_loop(onion_addr, url_token, short_addr, onion_pub_key, self.queues, session, settings)) self.assertEqual(self.queues[DST_MESSAGE_QUEUE].qsize(), 2) # Message and file self.assertEqual(self.queues[GROUP_MSG_QUEUE].qsize(), 5) # 5 group management messages
def start_key_exchange( onion_pub_key: bytes, # Public key of contact's v3 Onion Service nick: str, # Contact's nickname contact_list: 'ContactList', # Contact list object settings: 'Settings', # Settings object queues: 'QueueDict' # Dictionary of multiprocessing queues ) -> None: """Start X448 key exchange with the recipient. This function first creates the X448 key pair. It then outputs the public key to Relay Program on Networked Computer, that passes the public key to contact's Relay Program. When contact's public key reaches the user's Relay Program, the user will manually copy the key into their Transmitter Program. The X448 shared secret is used to create unidirectional message and header keys, that will be used in forward secret communication. This is followed by the fingerprint verification where the user manually authenticates the public key. Once the fingerprint has been accepted, this function will add the contact/key data to contact/key databases, and export that data to the Receiver Program on Destination Computer. The transmission is encrypted with the local key. --- TFC provides proactive security by making fingerprint verification part of the key exchange. This prevents the situation where the users don't know about the feature, and thus helps minimize the risk of MITM attack. The fingerprints can be skipped by pressing Ctrl+C. This feature is not advertised however, because verifying fingerprints the only strong way to be sure TFC is not under MITM attack. When verification is skipped, TFC marks the contact's X448 keys as "Unverified". The fingerprints can later be verified with the `/verify` command: answering `yes` to the question on whether the fingerprints match, marks the X448 keys as "Verified". Variable naming: tx = user's key rx = contact's key fp = fingerprint mk = message key hk = header key """ if not contact_list.has_pub_key(onion_pub_key): contact_list.add_contact(onion_pub_key, nick, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), KEX_STATUS_PENDING, settings.log_messages_by_default, settings.accept_files_by_default, settings.show_notifications_by_default) contact = contact_list.get_contact_by_pub_key(onion_pub_key) # Generate new private key or load cached private key if contact.tfc_private_key is None: tfc_private_key_user = X448.generate_private_key() else: tfc_private_key_user = contact.tfc_private_key try: tfc_public_key_user = X448.derive_public_key(tfc_private_key_user) # Import public key of contact while True: public_key_packet = PUBLIC_KEY_DATAGRAM_HEADER + onion_pub_key + tfc_public_key_user queue_to_nc(public_key_packet, queues[RELAY_PACKET_QUEUE]) tfc_public_key_contact = get_b58_key(B58_PUBLIC_KEY, settings, contact.short_address) if tfc_public_key_contact != b'': break # Validate public key of contact if len(tfc_public_key_contact) != TFC_PUBLIC_KEY_LENGTH: m_print([ "Warning!", "Received invalid size public key.", "Aborting key exchange for your safety." ], bold=True, tail=1) raise FunctionReturn("Error: Invalid public key length", output=False) if tfc_public_key_contact == bytes(TFC_PUBLIC_KEY_LENGTH): # The public key of contact is zero with negligible probability, # therefore we assume such key is malicious and attempts to set # the shared key to zero. m_print([ "Warning!", "Received a malicious zero-public key.", "Aborting key exchange for your safety." ], bold=True, tail=1) raise FunctionReturn("Error: Zero public key", output=False) # Derive shared key dh_shared_key = X448.shared_key(tfc_private_key_user, tfc_public_key_contact) # Domain separate unidirectional keys from shared key by using public # keys as message and the context variable as personalization string. tx_mk = blake2b(tfc_public_key_contact, dh_shared_key, person=b'message_key', digest_size=SYMMETRIC_KEY_LENGTH) rx_mk = blake2b(tfc_public_key_user, dh_shared_key, person=b'message_key', digest_size=SYMMETRIC_KEY_LENGTH) tx_hk = blake2b(tfc_public_key_contact, dh_shared_key, person=b'header_key', digest_size=SYMMETRIC_KEY_LENGTH) rx_hk = blake2b(tfc_public_key_user, dh_shared_key, person=b'header_key', digest_size=SYMMETRIC_KEY_LENGTH) # Domain separate fingerprints of public keys by using the # shared secret as key and the context variable as # personalization string. This way entities who might monitor # fingerprint verification channel are unable to correlate # spoken values with public keys that they might see on RAM or # screen of Networked Computer: Public keys can not be derived # from the fingerprints due to preimage resistance of BLAKE2b, # and fingerprints can not be derived from public key without # the X448 shared secret. Using the context variable ensures # fingerprints are distinct from derived message and header keys. tx_fp = blake2b(tfc_public_key_user, dh_shared_key, person=b'fingerprint', digest_size=FINGERPRINT_LENGTH) rx_fp = blake2b(tfc_public_key_contact, dh_shared_key, person=b'fingerprint', digest_size=FINGERPRINT_LENGTH) # Verify fingerprints try: if not verify_fingerprints(tx_fp, rx_fp): m_print([ "Warning!", "Possible man-in-the-middle attack detected.", "Aborting key exchange for your safety." ], bold=True, tail=1) raise FunctionReturn("Error: Fingerprint mismatch", delay=2.5, output=False) kex_status = KEX_STATUS_VERIFIED except (EOFError, KeyboardInterrupt): m_print([ "Skipping fingerprint verification.", '', "Warning!", "Man-in-the-middle attacks can not be detected", "unless fingerprints are verified! To re-verify", "the contact, use the command '/verify'.", '', "Press <enter> to continue." ], manual_proceed=True, box=True, head=2) kex_status = KEX_STATUS_UNVERIFIED # Send keys to the Receiver Program c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH) command = (KEY_EX_ECDHE + onion_pub_key + tx_mk + rx_mk + tx_hk + rx_hk + str_to_bytes(nick)) queue_command(command, settings, queues) while True: purp_code = ask_confirmation_code('Receiver') if purp_code == c_code.hex(): break elif purp_code == '': phase("Resending contact data", head=2) queue_command(command, settings, queues) phase(DONE) print_on_previous_line(reps=5) else: m_print("Incorrect confirmation code.", head=1) print_on_previous_line(reps=4, delay=2) # Store contact data into databases contact.tfc_private_key = None contact.tx_fingerprint = tx_fp contact.rx_fingerprint = rx_fp contact.kex_status = kex_status contact_list.store_contacts() queues[KEY_MANAGEMENT_QUEUE].put( (KDB_ADD_ENTRY_HEADER, onion_pub_key, tx_mk, csprng(), tx_hk, csprng())) m_print(f"Successfully added {nick}.", bold=True, tail_clear=True, delay=1, head=1) except (EOFError, KeyboardInterrupt): contact.tfc_private_key = tfc_private_key_user raise FunctionReturn("Key exchange interrupted.", tail_clear=True, delay=1, head=2)
def test_zero_public_key_raises_critical_error(self): with self.assertRaises(SystemExit): X448.shared_key(X448PrivateKey.generate(), bytes(TFC_PUBLIC_KEY_LENGTH))