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  ? 

  • 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

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

Children
  • 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. 

  • 'bt_le_oob_get_local' fetches the bt_le_oob object generated behind the scenes, and if BT_TESTING and BT_OOB_DATA_FIXED are set it overrides that random generation with a fixed value, but a 'bt_le_oob' is both a random value, and a confirm value that is related to the random value and the generated public key.

    'Local' and 'Remote' are cross wired so even if only one piece of OOB data is being exchanged, it is local to one device, and remote to the other, and for the remote device the expected confirm value is the one based on the peer public key, not the one internally generated. Even if both devices are running DATA_FIXED to see the same random value, they will have different confirm values that are not interchangeable.

    If you were to push that example to escalate the connection to security level 4, I believe it would fail validation of the OOB data even if the random value matches. At least that was my experience until I started generating the confirm values using the remote public key.

    My example is fully working, it just requires changes to the SDK that will get a bit more complicated if I ever need to handle more then one connection.

Related