def test_mitm_capable_devices_create_mitm_secured_link(central, peripheral): """If a MITM-resistant level is requested and devices have the capabilities, then it's achieved""" central_ss, peripheral_ss = init_security_sessions( central, peripheral, initiator_mitm=True, initiator_io_caps="IO_CAPS_KEYBOARD_ONLY", responder_mitm=True, responder_io_caps="IO_CAPS_DISPLAY_ONLY") central_ss.connect(peripheral_ss) # Always start peripheral_ss first peripheral_ss.wait_for_event() central_ss.start_pairing() # Accept request & perform MITM protection peripheral_ss.expect_pairing_request() peripheral_ss.accept_pairing_request() passkey = peripheral_ss.expect_passkey_display() peripheral_ss.wait_for_event() central_ss.enter_passkey(passkey) peripheral_ss.expect_pairing_success() # Now it should succeed central_ss.request_encryption( SecuritySession.ENCRYPTION_ENCRYPTED_WITH_MITM) central_ss.expect_encryption_changed( SecuritySession.ENCRYPTION_ENCRYPTED_WITH_MITM)
def test_generate_whitelist_from_bonded_devices(central, peripheral): """Generate whitelist based on bonded devices""" central_ss, peripheral_ss = init_security_sessions(central, peripheral) # generation should create an empty whitelist result = central.securityManager.generateWhitelistFromBondTable.withRetcode( 0)().result assert len(result) == 0 # remember responder address which will later end up in the whitelist responder_address = peripheral.gap.getAddress.withRetcode(0)().result # connect and pair to create a bond central_ss.connect(peripheral_ss) central_ss.start_pairing() central_ss.expect_pairing_success() sleep(1) central.gap.disconnect(central_ss.connection_handle, "USER_TERMINATION") # generation should crate whitelist with one entry result = central.securityManager.generateWhitelistFromBondTable.withRetcode( 0)().result assert len(result) == 2 # check entry against the responder address assert responder_address in result
def test_pairing_fail_if_passkey_wrong(central, peripheral): """MITM with IO_CAPS_KEYBOARD_ONLY on both ends and two different passkeys are input should fail pairing""" central_ss, peripheral_ss = init_security_sessions( central, peripheral, initiator_mitm=True, initiator_io_caps="IO_CAPS_KEYBOARD_ONLY", responder_mitm=True, responder_io_caps="IO_CAPS_KEYBOARD_ONLY") central_ss.connect(peripheral_ss) # Always start peripheral_ss first peripheral_ss.wait_for_event() central_ss.start_pairing() # We should get a request peripheral_ss.expect_pairing_request() # Accept request peripheral_ss.accept_pairing_request() # Wait for passkey requests peripheral_ss.expect_passkey_request() central_ss.expect_passkey_request() # Input passkeys peripheral_ss.enter_passkey(123456, asynchronous=True) central_ss.enter_passkey(654321) # Both should fail here peripheral_ss.expect_pairing_failure() central_ss.expect_pairing_failure()
def perform_bonding_with_privacy_and_disconnect(peripheral, central): central_ss, peripheral_ss = init_security_sessions(central, peripheral) peripheral.gap.enablePrivacy(True) central.gap.enablePrivacy(True) central.gap.setCentralPrivacyConfiguration(False, "DO_NOT_RESOLVE") central.gap.setPeripheralPrivacyConfiguration(False, "DO_NOT_RESOLVE") peripheral.gap.setPeripheralPrivacyConfiguration(False, "DO_NOT_RESOLVE") peripheral.gap.setCentralPrivacyConfiguration(False, "DO_NOT_RESOLVE") central_ss.connect(peripheral_ss) # Always start peripheral_ss first peripheral_ss.wait_for_event() central_ss.start_pairing() # We should get a request peripheral_ss.expect_pairing_request() # Accept request, pairing should complete successfully peripheral_ss.accept_pairing_request() peripheral_ss.expect_pairing_success() # central_ss should see pairing complete successfully too central_ss.expect_pairing_success() central.gap.disconnect(peripheral_ss.connection_handle, "USER_TERMINATION") # FIXME: needs event from the peripheral and central that they have been disconnected sleep(0.5)
def test_pairing_combinations(central, peripheral, responder_io_caps, initiator_io_caps, secure_connections, keygen): """Pairing procedure should follow table 2.8 from BLE Core spec Vol 3, Part H, 2.3.5.1""" central_ss, peripheral_ss = init_security_sessions( central, peripheral, initiator_mitm=True, initiator_io_caps=initiator_io_caps, responder_mitm=True, responder_io_caps=responder_io_caps) # Get Secure connections support sc_initiator = central.securityManager.getSecureConnectionsSupport().result sc_responder = peripheral.securityManager.getSecureConnectionsSupport( ).result if secure_connections: # Skip if unsupported if not sc_initiator or not sc_responder: return else: # Skip if unsupported if sc_initiator and sc_responder: return # Allow legacy pairing central.securityManager.allowLegacyPairing(True).success() peripheral.securityManager.allowLegacyPairing(True).success() central_ss.connect(peripheral_ss) # Perform pairing keygen.test(central_ss, peripheral_ss)
def test_write_signed_data_unencrypted(central, peripheral): """Write data that requires signing over an unencrypted link""" central_ss, peripheral_ss = init_security_sessions(central, peripheral) server = peripheral.gattServer client = central.gattClient # build server and create a characteristic that requires signing signed_handle = create_signed_characteristic(server) # connect without pairing central_handle, peripheral_handle = central_ss.connect(peripheral_ss) # test that writes fail without encryption or signing change_value(server, client, central_handle, signed_handle, value="00000002", success=False, sign=False) # encrypt which will trigger pairing and bonding central_ss.request_encryption(SecuritySession.ENCRYPTION_ENCRYPTED) central_ss.expect_encryption_changed(SecuritySession.ENCRYPTION_ENCRYPTED) # Wait as we got no visibility on whether the keys have been exchanged or not sleep(1) # re-connect and test characteristics over unencrypted connection central.gap.disconnect(central_ss.connection_handle, "USER_TERMINATION") central_ss.connect(peripheral_ss) # test writing the characteristic with signing change_value(server, client, central_handle, signed_handle, value="00000002", success=True, sign=True) # writes without signing should fail change_value(server, client, central_handle, signed_handle, value="00000003", success=False, sign=False)
def test_unsecured_link(central, peripheral): """Asking for an unsecured link should succeed immediately""" central_ss, peripheral_ss = init_security_sessions(central, peripheral) central_ss.connect(peripheral_ss) # Always start peripheral_ss first central_ss.request_encryption(SecuritySession.ENCRYPTION_NOT_ENCRYPTED) # It should succeed immediately central_ss.expect_encryption_changed( SecuritySession.ENCRYPTION_NOT_ENCRYPTED)
def test_secure_connections_pairing_fails_if_comparison_fails( central, peripheral): """In Secure Connections pairing, when doing numeric comparison and rejecting comparison, pairing should fail""" central_ss, peripheral_ss = init_security_sessions( central, peripheral, initiator_mitm=True, initiator_io_caps="IO_CAPS_DISPLAY_YESNO", responder_mitm=True, responder_io_caps="IO_CAPS_DISPLAY_YESNO") # Get Secure connections support sc_initiator = central.securityManager.getSecureConnectionsSupport().result sc_responder = peripheral.securityManager.getSecureConnectionsSupport( ).result # Skip if unsupported if not sc_initiator or not sc_responder: return central_ss.connect(peripheral_ss) # Always start peripheral_ss first peripheral_ss.wait_for_event() central_ss.start_pairing() # We should get a request peripheral_ss.expect_pairing_request() # Accept request peripheral_ss.accept_pairing_request() # Get passkeys (should be identical) passkey1 = peripheral_ss.expect_passkey_display() passkey2 = central_ss.expect_passkey_display() assert passkey1 == passkey2 # Expect confirmation requests # FIXME: too slow to set that expectation :( # peripheral_ss.expect_confirmation_request() # central_ss.expect_confirmation_request() # Reject on one end peripheral_ss.enter_confirmation(True, asynchronous=True) central_ss.enter_confirmation(False) # Both should fail here peripheral_ss.expect_pairing_failure() central_ss.expect_pairing_failure()
def test_encryption_triggers_pairing(central, peripheral): """Asking for a link should trigger pairing and then succeed""" central_ss, peripheral_ss = init_security_sessions(central, peripheral) central_ss.connect(peripheral_ss) # Always start peripheral_ss first peripheral_ss.wait_for_event() central_ss.request_encryption(SecuritySession.ENCRYPTION_ENCRYPTED) sleep(1) # Accept request, pairing should complete successfully peripheral_ss.accept_pairing_request() peripheral_ss.expect_pairing_success() # It should succeed central_ss.expect_encryption_changed(SecuritySession.ENCRYPTION_ENCRYPTED)
def test_responder_can_reject_pairing_request(central, peripheral): """A slave should be able to reject a pairing request""" central_ss, peripheral_ss = init_security_sessions(central, peripheral) central_ss.connect(peripheral_ss) # Always start peripheral_ss first peripheral_ss.wait_for_event() central_ss.start_pairing() # We should get a request peripheral_ss.expect_pairing_request() # Reject pairing request peripheral_ss.reject_pairing_request() # central_ss should see pairing fail central_ss.expect_pairing_failure()
def test_non_mitm_capable_device_create_non_mitm_secured_link( central, peripheral): """If a MITM-resistant level is requested and devices don't have the capabilities, then an 'encrypted' level should be achieved instead """ central_ss, peripheral_ss = init_security_sessions(central, peripheral) central_ss.connect(peripheral_ss) # Always start peripheral_ss first peripheral_ss.wait_for_event() central_ss.request_encryption( SecuritySession.ENCRYPTION_ENCRYPTED_WITH_MITM) # Accept request, pairing should complete successfully peripheral_ss.accept_pairing_request() # It should succeed central_ss.expect_encryption_changed(SecuritySession.ENCRYPTION_ENCRYPTED)
def test_pairing_fail_if_rejected(central, peripheral): """Pairing should fail with default parameters on both ends if request is rejected""" central_ss, peripheral_ss = init_security_sessions(central, peripheral) central_ss.connect(peripheral_ss) # Always start peripheral_ss first peripheral_ss.wait_for_event() central_ss.start_pairing() # We should get a request peripheral_ss.expect_pairing_request() # Reject request (method is still successful) peripheral_ss.reject_pairing_request() # central_ss should see pairing fail central_ss.expect_pairing_failure()
def test_pairing_with_defaults(central, peripheral): """Pairing should succeed with default parameters on both ends""" central_ss, peripheral_ss = init_security_sessions(central, peripheral) central_ss.connect(peripheral_ss) # Always start peripheral_ss first peripheral_ss.wait_for_event() central_ss.start_pairing() # We should get a request peripheral_ss.expect_pairing_request() # Accept request, pairing should complete successfully peripheral_ss.accept_pairing_request() peripheral_ss.expect_pairing_success() # central_ss should see pairing complete successfully too central_ss.expect_pairing_success()
def test_write_signed_data_encrypted(central, peripheral): """Write data that requires signing over an encrypted link""" central_ss, peripheral_ss = init_security_sessions(central, peripheral) server = peripheral.gattServer client = central.gattClient # build server and create a characteristic that requires signing signed_handle = create_signed_characteristic(server) # connect without pairing central_handle, peripheral_handle = central_ss.connect(peripheral_ss) # encrypt which will trigger pairing and bonding central_ss.request_encryption(SecuritySession.ENCRYPTION_ENCRYPTED) central_ss.expect_encryption_changed(SecuritySession.ENCRYPTION_ENCRYPTED) # test characteristics over encrypted connection change_value(server, client, central_handle, signed_handle, value="00000002", success=True, sign=True)
def test_secure_link_succeeds_immediately_if_paired(central, peripheral): """If pairing was successful then asking for a secure link should succeed immediately""" central_ss, peripheral_ss = init_security_sessions(central, peripheral) central_ss.connect(peripheral_ss) # Always start peripheral_ss first peripheral_ss.wait_for_event() central_ss.start_pairing() # We should get a request peripheral_ss.expect_pairing_request() # Accept request, pairing should complete successfully peripheral_ss.accept_pairing_request() peripheral_ss.expect_pairing_success() # central_ss should see pairing complete successfully too central_ss.expect_pairing_success() # It should succeed central_ss.request_encryption(SecuritySession.ENCRYPTION_ENCRYPTED) central_ss.expect_encryption_changed(SecuritySession.ENCRYPTION_ENCRYPTED)
def test_downgrade_of_security_fails(central, peripheral): """Asking for an encryption level downgrade should fail""" central_ss, peripheral_ss = init_security_sessions(central, peripheral) central_ss.connect(peripheral_ss) # Always start peripheral_ss first peripheral_ss.wait_for_event() central_ss.start_pairing() # We should get a request peripheral_ss.expect_pairing_request() # Accept request, pairing should complete successfully peripheral_ss.accept_pairing_request() peripheral_ss.expect_pairing_success() # central_ss should see pairing complete successfully too central_ss.expect_pairing_success() # It should fail central_ss.request_encryption_expect_failure( SecuritySession.ENCRYPTION_NOT_ENCRYPTED, -1)
def test_perform_pairing_procedure_policy(central, peripheral): """validate that when a peripheral with privacy enabled with the policy PERFORM_PAIRING_PROCEDURE is connected by an unknown privacy enabled central then the pairing procedure is performed""" central_ss, peripheral_ss = init_security_sessions(central, peripheral) peripheral.gap.enablePrivacy(True) central.gap.enablePrivacy(True) peripheral.gap.setPeripheralPrivacyConfiguration( False, "PERFORM_PAIRING_PROCEDURE") central.gap.setCentralPrivacyConfiguration(False, "DO_NOT_RESOLVE") central_ss.connect(peripheral_ss) # We should get a request automatically because of the policy peripheral_ss.wait_for_event() peripheral_ss.expect_pairing_request() # Accept request, pairing should complete successfully peripheral_ss.accept_pairing_request() peripheral_ss.expect_pairing_success()
def test_recovering_keys(central, peripheral): """Change databases and recover old keys""" central_ss, peripheral_ss = init_security_sessions(central, peripheral) central.ble.createFilesystem() peripheral.ble.createFilesystem() # we create a new database and store keys in this one peripheral.securityManager.setDatabaseFilepath("/fs/sm_db1") central.securityManager.setDatabaseFilepath("/fs/sm_db1") peripheral.securityManager.preserveBondingStateOnReset(True) central.securityManager.preserveBondingStateOnReset(True) central_ss.connect(peripheral_ss) peripheral_ss.wait_for_event() # pair to bond and save the ltk in the /fs/sm_db1 for later central_ss.start_pairing() # accept request peripheral_ss.expect_pairing_request() peripheral_ss.accept_pairing_request() peripheral_ss.expect_pairing_success() # initiator should see pairing succeed central_ss.expect_pairing_success() # we will reconnect central.gap.disconnect(central_ss.connection_handle, "USER_TERMINATION") sleep(1) # but change database so that it doesn't have the keys peripheral.securityManager.setDatabaseFilepath("/fs/sm_db2") central.securityManager.setDatabaseFilepath("/fs/sm_db2") # now reconnect central_ss.connect(peripheral_ss) peripheral_ss.wait_for_event() # encrypt which will trigger pairing and bonding and prove we have no keys central_ss.request_encryption(SecuritySession.ENCRYPTION_ENCRYPTED) # reject request peripheral_ss.expect_pairing_request() peripheral_ss.accept_pairing_request() # initiator should see pairing fail peripheral_ss.expect_pairing_success() # we will reconnect central.gap.disconnect(central_ss.connection_handle, "USER_TERMINATION") sleep(1) # but first change database to recover old keys peripheral.securityManager.setDatabaseFilepath("/fs/sm_db1") central.securityManager.setDatabaseFilepath("/fs/sm_db1") # now reconnect central_ss.connect(peripheral_ss) # wait for a pairing request that should never come peripheral.securityManager.setPairingRequestAuthorisation(True).success() pairing_result = peripheral.securityManager.waitForEvent.setAsync( ).withRetcode(-1)(peripheral_ss.connection_handle, 1000) # encrypt with existing keys we found in the db (this should NOT trigger pairing) central_ss.request_encryption(SecuritySession.ENCRYPTION_ENCRYPTED) central_ss.expect_encryption_changed(SecuritySession.ENCRYPTION_ENCRYPTED) # make sure no pairing happened assert not pairing_result.success() assert "Pairing timeout" == pairing_result.result