LESC Static OOB?

I am working in NCS v2.7.0 for nRF52833 DevKits, and trying to set up a secure BLE connection to allow remote control of the device over GATT.

My devices are headless IOT devices, so there is no possibility for a true OOB data pathway and no user nearby to provide feedback, so none of the basic examples quite work. My understanding is that out of the box I would need to either use legacy static OOB which exposes me to a lack of forward secrecy, or use LESC with a static passkey which exposes me to brute force attacks.

What I would like to do is provide a fixed 16-byte random seed to an LE SC OOB data structure (bt_le_oob_sc_data.r), and have the device compute the confirm value based on the current public key. This would let me leverage ephemeral DH keys from LESC ensuring forward secrecy, and a 128-bit shared secret is a lot harder to brute force then a 6-digit static pin. I understand that true OOB is much better, but the actual alternatives available to me are much worse.

First, before I get into my mess, is there a native way to accomplish what I'm trying to do that I'm missing?

If not, what I did was hack a hook into 'smp.c' to allow retrieval of the 'sc_public_key' value:

const uint8_t *bt_smp_get_sc_public_key(void)
{
    return sc_public_key;
}


Then in my application, I added the following code to my initiator (central device) to re-calculate the confirm value on each request:

#include "crypto/bt_crypto.h"

// Hacked into the SDK smp.c to fetch the static 'sc_public_key' value
extern const uint8_t *bt_smp_get_sc_public_key(void);

static struct bt_le_oob_sc_data oob_local;
static const uint8_t static_r[16] = {
    0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
    0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE
};

static void oob_data_request(struct bt_conn *conn, struct bt_conn_oob_info *info)
{
	int err;
	
    if (info->type != BT_CONN_OOB_LE_SC) {
        printk("OOB type not LE SC, rejected\n");
        return;
    }

    printk("Central: OOB data requested\n");

	memcpy(oob_local.r, static_r, 16);
	err = bt_crypto_f4(bt_smp_get_sc_public_key(), bt_smp_get_sc_public_key(), &oob_local.r, 0, &oob_local.c);
	if (err) {
		printk("Error generating OOB confirm value: %d\n", err);
		return;
	}

	err = bt_le_oob_set_sc_data(conn, &oob_local, NULL);
	if (err) {
		printk("Error while setting OOB data: %d\n", err);
	}
}

I have basically the same thing added to the peripheral device, except argument 4 into 'bt_crypto_f4' is set to 1 instead of 0 to reflect the different role.

I think this should work, but my pairing request always end with a status 0xB (BT_SMP_ERR_DHKEY_CHECK_FAILED). Am I missing something, or is what I'm trying to do just not possible.

Thanks,
Jeremy

Parents
  • Hi Jeremy, 

    We would need to check if the OOB communication is both way or one way only. Normally on our board it's usually BT_CONN_OOB_LOCAL_ONLY meaning the OOB data r&c  is to be offered from the nRF52 and being read by the peer. 
    Could you take a look at this ticket: Factory-pairing two nRF52840 

    I think it's very similar to what you want to achieve. Please take a look at the peripheral_hids_keyboard.
    Could you try to test that example with CONFIG_BT_OOB_DATA_FIXED  ? 

Reply
  • Hi Jeremy, 

    We would need to check if the OOB communication is both way or one way only. Normally on our board it's usually BT_CONN_OOB_LOCAL_ONLY meaning the OOB data r&c  is to be offered from the nRF52 and being read by the peer. 
    Could you take a look at this ticket: Factory-pairing two nRF52840 

    I think it's very similar to what you want to achieve. Please take a look at the peripheral_hids_keyboard.
    Could you try to test that example with CONFIG_BT_OOB_DATA_FIXED  ? 

Children
  • Got it working!

    Tested a bit and I believe that 'LOCAL_ONLY' will only work if the other side is expecting the connection to be 'REMOTE_ONLY', which still requires some transfer of data between the two devices. In the hids example for instance, one part of the setup is to copy the random/confirm values from the central device.

    However, before the OOB data check, both devices do exchange public keys, and all you need to generate a 'confirm' value is a public key and the right 16 'random' bytes.

    I did need to poke two holes into smp.c; one to fetch the locally generated public key for local confirmation, and one for the remote public key associated with the current connection for the remote confirmation. Currently my remote fetch code is cheating a bit and blindly fetching index 0 from the smp pool, but currently I have 'CONFIG_BT_MAX_CONN' set to 1 so this works.

    smp.c hacks:

    const uint8_t *bt_smp_get_sc_public_key(void)
    {
        return sc_public_key;
    }
    
    uint8_t *bt_smp_get_pkey(uint8_t smp_index)
    {
    	return (uint8_t*)&bt_smp_pool[smp_index].pkey;
    }

    Updated 'oob_data_request' handler. Identical for both sides:

    #include "crypto/bt_crypto.h"
    
    // Hacked into the SDK smp.c to fetch the local 'sc_public_key' value
    extern const uint8_t *bt_smp_get_sc_public_key(void);
    // Hacked into the SDK smp.c to fetch the remote public key for the current connection
    extern uint8_t *bt_smp_get_pkey(uint8_t smp_index);
    
    static struct bt_le_oob_sc_data oob_local;
    static struct bt_le_oob_sc_data oob_remote;
    static const uint8_t static_r[16] = {
        0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
        0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE
    };
    
    
    static void oob_data_request(struct bt_conn *conn, struct bt_conn_oob_info *info)
    {
    	printk("OOB data requested\n");
    
        if (info->type != BT_CONN_OOB_LE_SC) {
            printk("OOB type not LE SC, rejected\n");
            return;
        }
    
    	if (info->lesc.oob_config != BT_CONN_OOB_BOTH_PEERS) {
    		printk("LESC OOB config not supported (oob_config=%d)\n", info->lesc.oob_config);
    		return;
    	}
    
    	int err;
    	uint8_t * pkey = bt_smp_get_pkey(0);
    
    	// Compute oob_remote 'confirm' value using pkey from the smp_public_key message
    	memcpy(oob_remote.r, static_r, 16);
    	err = bt_crypto_f4(pkey, pkey, &oob_remote.r, 0, &oob_remote.c);
    	if (err) {
    		printk("Error generating remote OOB confirm value: %d\n", err);
    		return;
    	}
    
    	// Computer oob_local 'confirm' value using the public key generated locally for this connection
    	memcpy(oob_local.r, static_r, 16);
    	err = bt_crypto_f4(bt_smp_get_sc_public_key(), bt_smp_get_sc_public_key(), &oob_local.r, 0, &oob_local.c);
    	if (err) {
    		printk("Error generating local OOB confirm value: %d\n", err);
    		return;
    	}
    
    	err = bt_le_oob_set_sc_data(conn, &oob_local, &oob_remote);
    	if (err) {
    		printk("Error while setting OOB data: %d\n", err);
    	}
    }

    Is there any more elegant way to accomplish this? Or is there some way to infer 'bt_smp_pool' index based on the 'conn' passed into the 'oob_data_request' callback? I could extend the callback to send additional details back, but I'm trying to limit my SDK changes.

    Thanks,
    Jeremy

Related