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

  • So I've done some more digging, and think I'm missing something more basic. I've removed my own attempts to inject a static 'r' value and instead just set CONFIG_BT_TESTING and CONFIG_BT_OOB_DATA_FIXED to 'y' in 'prj.conf'. This appears to effectively do what I was trying to do, only with a fixed-in-the-SDK static value ... and is still failing the same as before.

    The new simplified code implemented on both central and peripheral now is:

    static struct bt_le_oob oob_local_full;
    
    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("OOB data requested\n");
    	
    	err = bt_le_oob_set_sc_data(conn, &oob_local_full.le_sc_data, NULL);
    	if (err) {
    		printk("Error while setting OOB data: %d\n", err);
    	}
    }
    
    
    int main(void)
    {
    	...
    	
    	bt_le_oob_get_local(0, &oob_local_full);
    
    	...
    }

    With debugging I can confirm this leads to identical 'r' values being seeded onto both devices and being supplied in smp->oodb_local, but they are still returning status 0xB. Any help would be appreciated.

    Thanks,
    Jeremy

  • 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  ? 

  • 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

  • Hi Jeremy, 
    Sorry for the late reply. 
    I did a quick test here with the peripheral_nfc_pairing sample and I can see that I have the same local random oob (1 2 3 4 5 ...) as expected from the code in smp.c : 

    here is where I printed it out: 

    I simply added this in to prj.conf: 

    CONFIG_BT_OOB_DATA_FIXED=y
    CONFIG_BT_TESTING=y

    My assumption is that having a hardcoded r value is all it need to be able to do the static OOB ? And you don't have to calculate the sc key on  your own in the application as you showed in the last reply ? 
  • Update: I see your previous replied that you mentioned it couldn't work. I will have to check it out but we don't have a NFC reader to test with the central_nfc_pairing so it can be difficult. My suspicion is that you may need to make sure it's only one way communication and on the other side you need to set the random value to 0. 

Related