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

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

  • If you control both sides, why don't you just skip the standard BLE pairing step altogether and generate LTK in your own way?

    If the two devices initially share a 16 byte secret S (as I assume they do from your question), you can generate an LTK by first performing ECDH resulting in shared secret E, then let the LTK be for example HMAC(S, E). That way you get forward secrecy.

    Another way would be to use standard LESC numeric comparison. You can encrypt the 6 digits that are supposed to be shown on the display with your shared key and some AEAD cipher, send it over BLE characteristic to the other device. After decrypting it, accept if correct. Same in the other direction. Just make sure the Additional Data is different for every direction to avoid reflection attack.

  • That first suggestion makes sense conceptually, but how would that be implemented with NCS? Would you just make a new simplified security manager to handle that process and set up the connection, or is there a way to use the SDK's 'smp' module and set your own LTK?

  • You skip the Security Manager Protocol entirely and install the LTK directly. I'm not super familiar with the SDK here but it seems you can use the `bt_keys_store` function, which is otherwise used by the SMP module when pairing finishes. See https://github.com/nrfconnect/sdk-zephyr/blob/main/subsys/bluetooth/host/keys.h#L188. You can do your ECDH exchange over some custom GATT characteristic or L2CAP CoC.

    See https://devzone.nordicsemi.com/f/nordic-q-a/112415/connection-using-old-pairing-information-saved/491888 for an example. Note that you should simulate (mark) that the key was generated using LESC, so set the `keys` field to `BT_KEYS_LTK_P256`, `flags` to `BT_KEYS_SC`, `enc_size` to `BT_SMP_MAX_ENC_KEY_SIZE` and ediv and rand to 0.

Related