Beware that this post is related to an SDK in maintenance mode
More Info: Consider nRF Connect SDK for new designs
This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

pc-ble-driver-py - BLEGapSecStatus.dhkey_failure

Hi,

I'm using an NRF51 Dongle as a central to connect to a BLE peripheral using LE Secure passkey pairing. I've got to scanning / connecting successfully, I'm just stuck on the pairing.

It gets to the point where it sends the SMP Pairing Public Key but it's all 0's, so the peripheral rejects it with a DHKey Check Failed:

I've used the heart rate collector and a few examples around to get to this point, here's my pairing function:

if self.conn_handle is None:
            return False
            
kdist_own   = BLEGapSecKDist(enc  = 1,
                             id   = 1,
                             sign = 0,
                             link = 0)
                             
kdist_peer  = BLEGapSecKDist(enc  = 1,
                             id   = 1,
                             sign = 0,
                             link = 0)

sec_params  = BLEGapSecParams(bond          = True,
                              mitm          = True,
                              lesc          = True,
                              oob           = False,
                              keypress      = False,
                              io_caps       = BLEGapIOCaps.keyboard_display,
                              min_key_size  = 7,
                              max_key_size  = 16,
                              kdist_own     = kdist_own,
                              kdist_peer    = kdist_peer)

self.adapter.driver.ble_gap_authenticate(self.conn_handle, sec_params)

self.adapter.evt_sync[self.conn_handle].wait(BLEEvtID.gap_evt_sec_params_request)
self.adapter.driver.ble_gap_sec_params_reply(self.conn_handle,
                                             BLEGapSecStatus.success,
                                             sec_params = None)
                                
result = self.adapter.evt_sync[self.conn_handle].wait(BLEEvtID.gap_evt_auth_status)

if result['auth_status'] ==  BLEGapSecStatus.success:
    self.keyset = adapter.db_conns[self.conn_handle]._keyset
    return True        

return False

think the issue is around the keyset that's being used in the call to ble_gap_sec_params_reply but I can't figure out how to get the keys set to something non-zero.

I get an error in the debug logging after the gap_evt_auth_status comes back:

evt> auth_status conn(0)
 error_src(1)
 bonded(0)
 sm1_levels(<pc_ble_driver_py.lib.nrf_ble_driver_sd_api_v2.ble_gap_sec_levels_t; proxy of <Swig Object of type 'ble_gap_sec_levels_t *' at 0x000001AC59C56780> >)
 sm2_levels(<pc_ble_driver_py.lib.nrf_ble_driver_sd_api_v2.ble_gap_sec_levels_t; proxy of <Swig Object of type 'ble_gap_sec_levels_t *' at 0x000001AC59C564B0> >)
 kdist_own(enc(0) id(1) sign(0) link(0))
 kdist_peer(enc(0) id(0) sign(0) link(0))
 auth_status(BLEGapSecStatus.dhkey_failure)

Full code:

import sys
import time
import logging
from pc_ble_driver_py import config
config.__conn_ic_id__ = "NRF51"
from queue import Queue, Empty
from pc_ble_driver_py.observers import *
import pc_ble_driver_py.lib.nrf_ble_driver_sd_api_v2 as driver
import pc_ble_driver_py.ble_driver_types as util

TARGET_DEV_NAME = "BLE_DEVICE"
CFG_TAG = 1





class HRCollector(BLEDriverObserver, BLEAdapterObserver):
    def __init__(self, adapter):
        super(HRCollector, self).__init__()
        self.adapter = adapter
        self.conn_q = Queue()
        self.adapter.observer_register(self)
        self.adapter.driver.observer_register(self)
        self.adapter.default_mtu = 250
        self.keyset = None
        self.conn_handle = None

    def open(self):
        self.adapter.driver.open()
        self.adapter.driver.ble_enable(BLEEnableParams(
                                            vs_uuid_count=5,
                                            service_changed=0,
                                            periph_conn_count=0,
                                            central_conn_count=1,
                                            central_sec_count=1))

    def close(self):
        self.adapter.driver.close()

    def connect_and_discover(self):
        scan_duration = 60
        params = BLEGapScanParams(interval_ms=200, window_ms=150, timeout_s=scan_duration)
        self.adapter.driver.ble_gap_scan_start(scan_params=params)

        try:
            new_conn = self.conn_q.get(timeout=scan_duration)
            self.adapter.service_discovery(new_conn)

            return new_conn
        except Empty:
            print(f"No device advertising with name {TARGET_DEV_NAME} found.")
            return None


    def on_gap_evt_connected(self, ble_driver, conn_handle, peer_addr, role, conn_params):
        print("New connection: {}".format(conn_handle))
        self.conn_q.put(conn_handle)

        self.conn_handle = conn_handle
    

    def on_gap_evt_auth_key_request(self, ble_driver, conn_handle, **kwargs):
        passkey = util.list_to_uint8_array([1, 1, 1, 1, 1, 1])

        print("passkey requested")

        driver.sd_ble_gap_auth_key_reply(
            ble_driver.rpc_adapter,
            conn_handle,
            kwargs['key_type'],
            passkey.cast(),
        ) 

    def on_gap_evt_disconnected(self, ble_driver, conn_handle, reason):
        print("Disconnected: {} {}".format(conn_handle, reason))
        self.conn_handle = None

    def on_gap_evt_adv_report(self, ble_driver, conn_handle, peer_addr, rssi, adv_type, adv_data):
        if BLEAdvData.Types.short_local_name in adv_data.records:
            dev_name_list = adv_data.records[BLEAdvData.Types.short_local_name]

            dev_name = "".join(chr(e) for e in dev_name_list)
            address_string = "".join("{0:02X}".format(b) for b in peer_addr.addr)
            print("Received advertisment report, address: 0x{}, device_name: {}".format(address_string, dev_name))

            if dev_name == TARGET_DEV_NAME:
                self.adapter.connect(peer_addr, tag=CFG_TAG)


    def authenticate(self):
        if self.conn_handle is None:
            return False
            
        kdist_own   = BLEGapSecKDist(enc  = 1,
                                     id   = 1,
                                     sign = 0,
                                     link = 0)
                                     
        kdist_peer  = BLEGapSecKDist(enc  = 1,
                                     id   = 1,
                                     sign = 0,
                                     link = 0)
        
        sec_params  = BLEGapSecParams(bond          = True,
                                      mitm          = True,
                                      lesc          = True,
                                      oob           = False,
                                      keypress      = False,
                                      io_caps       = BLEGapIOCaps.keyboard_display,
                                      min_key_size  = 7,
                                      max_key_size  = 16,
                                      kdist_own     = kdist_own,
                                      kdist_peer    = kdist_peer)
        
        self.adapter.driver.ble_gap_authenticate(self.conn_handle, sec_params)
        
        self.adapter.evt_sync[self.conn_handle].wait(BLEEvtID.gap_evt_sec_params_request)
        self.adapter.driver.ble_gap_sec_params_reply(self.conn_handle,
                                                     BLEGapSecStatus.success,
                                                     sec_params = None)
                                        
        result = self.adapter.evt_sync[self.conn_handle].wait(BLEEvtID.gap_evt_auth_status)

        if result['auth_status'] ==  BLEGapSecStatus.success:
            self.keyset = adapter.db_conns[self.conn_handle]._keyset
            return True        

        return False
















def init(conn_ic_id):
    global config,          \
        BLEDriver,          \
        BLEAdvData,         \
        BLEEvtID,           \
        BLEAdapter,         \
        BLEEnableParams,    \
        BLEGapTimeoutSrc,   \
        BLEGapIOCaps,       \
        BLEUUID,            \
        BLEUUIDBase,        \
        BLEConfigCommon,    \
        BLEConfig,          \
        BLEConfigConnGatt,  \
        BLEGapScanParams,   \
        BLEGapSecKDist,     \
        BLEGapSecParams,    \
        BLEGapSecStatus,    \
        BLEGapSecKeyset

    
    from pc_ble_driver_py.ble_driver import (
                BLEDriver,          
                BLEAdvData,         
                BLEEvtID,           
                BLEEnableParams,    
                BLEGapTimeoutSrc,   
                BLEGapIOCaps,       
                BLEUUID,            
                BLEUUIDBase,        
                BLEConfigCommon,    
                BLEConfig,          
                BLEConfigConnGatt,  
                BLEGapScanParams,   
                BLEGapSecKDist,     
                BLEGapSecParams,    
                BLEGapSecStatus,
                BLEGapSecKeyset,
                util,              
                driver
    )

    from pc_ble_driver_py.ble_adapter import BLEAdapter

    global nrf_sd_ble_api_ver
    nrf_sd_ble_api_ver = config.sd_api_ver_get()

def main(selected_serial_port):
    print("Serial port used: {}".format(selected_serial_port))
    driver = BLEDriver(serial_port=selected_serial_port, auto_flash=False, baud_rate=1000000, log_severity_level="debug")

    adapter = BLEAdapter(driver)
    collector = HRCollector(adapter)
    collector.open()
    conn = collector.connect_and_discover()
    collector.authenticate()

    if conn is not None:
        time.sleep(10)

    collector.close()


def item_choose(item_list):
    for i, it in enumerate(item_list):
        print("\t{} : {}".format(i, it))
    print(" ")

    while True:
        try:
            choice = int(input("Enter your choice: "))
            if (choice >= 0) and (choice < len(item_list)):
                break
        except Exception:
            pass
        print("\tTry again...")
    return choice


if __name__ == "__main__":
    logging.basicConfig(level="DEBUG", format="%(asctime)s [%(thread)d/%(threadName)s] %(message)s")
    serial_port = None

    init("NRF51")
    if len(sys.argv) == 3:
        serial_port = sys.argv[2]
    else:
        descs = BLEDriver.enum_serial_ports()
        choices = ["{}: {}".format(d.port, d.serial_number) for d in descs]
        choice = item_choose(choices)
        serial_port = descs[choice].port
    main(serial_port)
    quit()

Many thanks

Parents
  • Hi,

    Your implementation sets the lesc flag in sec_params, but you seem to still follow legacy pairing and not LE Secure Connections pairing for the pairing procedure. Note as part of "LESC Authentication Stage 1" you must answer to the event BLE_GAP_EVT_LESC_DHKEY_REQUEST with a call to sd_ble_gap_lesc_dhkey_reply(). As reference, here is the implementation used in nRF5 SDK v12.3, for the ble_app_multirole_lesc example (main.c, lines 677 through 685):

     677         case BLE_GAP_EVT_LESC_DHKEY_REQUEST:
     678             NRF_LOG_INFO("%s: BLE_GAP_EVT_LESC_DHKEY_REQUEST\r\n", nrf_log_push(roles_str[role]));
     679             peer_pk.p_le_data = &p_ble_evt->evt.gap_evt.params.lesc_dhkey_request.p_pk_peer->pk[0];
     680             peer_pk.len = BLE_GAP_LESC_P256_PK_LEN;
     681             err_code = nrf_crypto_shared_secret_compute(NRF_CRYPTO_CURVE_SECP256R1, &m_crypto_key_sk, &peer_pk, &m_crypto_key_dhkey);
     682             APP_ERROR_CHECK(err_code);
     683             err_code = sd_ble_gap_lesc_dhkey_reply(conn_handle, &m_lesc_dhkey);
     684             APP_ERROR_CHECK(err_code);
     685             break;

    Regards,
    Terje

  • Hi,

    Thanks for the reply, I did wonder if the BLE_GAP_EVT_LESC_DHKEY_REQUEST was the issue but that sequence diagram shows it coming after the SMP Pairing Public Key is sent so I assumed it couldn't be the problem. I'll put in an implementation for the DHKEY_REQUEST and let you know how it goes.

    Many thanks,

    Peter

  • Hi,

    There are multiple keys involved here, so one can easily get confused. As the logged error message include "BLEGapSecStatus.dhkey_failure", dhkey related issues is the prime suspect. Computation of the dhkey is the responsibility of the application, so you must perform the computation that in the SDK is performed by nrf_crypto_shared_secret_compute(). I am looking forward to hear how it went.

    Regards,
    Terje

Reply
  • Hi,

    There are multiple keys involved here, so one can easily get confused. As the logged error message include "BLEGapSecStatus.dhkey_failure", dhkey related issues is the prime suspect. Computation of the dhkey is the responsibility of the application, so you must perform the computation that in the SDK is performed by nrf_crypto_shared_secret_compute(). I am looking forward to hear how it went.

    Regards,
    Terje

Children
  • Hi again, 

    I did manage to get it all implemented in the end. It looks like the python bindings for the pc-ble-driver just doesn't have support for LE Secure pairing, or atleast it's only half implemented? There isn't any callback implemented for the BLE_GAP_EVT_LESC_DHKEY_REQUEST for example.

    Fortunately the guy who answered https://devzone.nordicsemi.com/f/nordic-q-a/57036/pc-ble-driver-py---blegapsecstatus-dhkey_failure had implemented pretty much all of it, apart from his was in python 2 and I'm using python 3 so I had to make some changes there. Also he uses pyelliptic which has been deprecated for some time, so that's not great but it's sort of ok just for the testing I'm doing.

    So in the end the authentication function looks like:

            kdist_own   = BLEGapSecKDist(enc  = 1,
                                         id   = 1,
                                         sign = 0,
                                         link = 0)
                                         
            kdist_peer  = BLEGapSecKDist(enc  = 1,
                                         id   = 1,
                                         sign = 0,
                                         link = 0)
            
            sec_params  = BLEGapSecParams(bond          = True,
                                          mitm          = True,
                                          lesc          = True,
                                          oob           = False,
                                          keypress      = False,
                                          io_caps       = BLEGapIOCaps.keyboard_display,
                                          min_key_size  = 7,
                                          max_key_size  = 16,
                                          kdist_own     = kdist_own,
                                          kdist_peer    = kdist_peer)
            
            self.adapter.driver.ble_gap_authenticate(self.conn_handle, sec_params)
            
            # Wait for gap_evt_sec_params_request
            self.adapter.evt_sync[self.conn_handle].wait(BLEEvtID.gap_evt_sec_params_request)
    
            pub_key = BLEGapLESCp256pk(pk = self.adapter.driver.ecc_create_keys())
            self.adapter.driver.ble_gap_sec_params_reply(self.conn_handle,
                                                         BLEGapSecStatus.success,
                                                         sec_params = None,
                                                         own_keys = pub_key)
    
            # Wait for gap_evt_lesc_dhkey_request
            result = self.adapter.evt_sync[self.conn_handle].wait(BLEEvtID.gap_evt_lesc_dhkey_request)
    
            dhkey = BLEGapLESCdhkey(key = self.adapter.driver.ecc_get_dhkey(result['p_pk_peer'].pk))
            self.adapter.driver.ble_gap_lesc_dhkey_reply(self.conn_handle, dhkey)
    
            # Wait for gap_evt_auth_status
            result = self.adapter.evt_sync[self.conn_handle].wait(BLEEvtID.gap_evt_auth_status)
    
            if result['auth_status'] ==  BLEGapSecStatus.success:
                self.keyset = adapter.db_conns[self.conn_handle]._keyset
                return True

    But you have to make some changes in the ble_driver.py and the ble_adapter.py to get to that point.

    4503.ble_adapter.patch

    3513.ble_driver.patch

    Thanks,

    Peter

  • Hi,

    I am happy to see that you got there in the end. Thanks for sharing your solution, it is highly appreciated!

    Regards,
    Terje

Related