This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

Adding Encryption to Secure DFU SDK v15

In the following Q&A I will outline a working implementation of encrypted firmware updates using the Secure DFU in SDK v15. This implementation allows for encrypted firmware updates of both the application and the bootloader.


Creating a custom DFU init packet

We need a unique nonce/IV for each firmware version to avoid reuse. Therefore the init packet generated by nrfuitl needs to be extended to include the nonce. You can either customize nrfutil as outlined here: http://infocenter.nordicsemi.com/topic/com.nordic.infocenter.tools/dita/tools/nrfutil/nrfutil_customizing.html?cp=5_5_8; you can use the version of nrfutil.exe (v3.5.1) found below (which has been modified to generate a new random 12 byte nonce (created with a call to os.urandom()) and include it in the init packet each time a call to 'nrfutil pkg generate' is made) or make the changes to the nrfutil source code (v3.5.1) as outlined below:

Replace lines 234-254 of package.py with:

|- Image #{0}:
   |- Type: {1}
   |- Image file: {2}
   |- Init packet file: {3}
      |
      |- op_code: {4}
      |- signature_type: {5}
      |- signature (little-endian): {6}
      |
      |- fw_version: 0x{7:08X} ({7})
      |- hw_version 0x{8:08X} ({8})
      |- sd_req: {9}
      |- type: {10}
      |- sd_size: {11}
      |- bl_size: {12}
      |- app_size: {13}
      |
      |- hash_type: {14}
      |- hash (little-endian): {15}
      |
      |- is_debug: {16}
      |- nonce (little-endian): {17}

Replace lines 256-273 of package.py with:

""".format(index,
        type_strs[hex_type],
        img.bin_file,
        img.dat_file,
        CommandTypes(cmd.op_code).name,
        signature_type,
        signature_hex,
        cmd.init.fw_version,
        cmd.init.hw_version,
        sd_req,
        DFUType(cmd.init.type).name,
        cmd.init.sd_size,
        cmd.init.bl_size,
        cmd.init.app_size,
        HashTypes(cmd.init.hash.hash_type).name,
        binascii.hexlify(cmd.init.hash.hash),
        cmd.init.is_debug,
        binascii.hexlify(cmd.init.nonce)
        )

Add the following between line 353 & 355 in package.py:

nonce = os.urandom(12)

Replace lines 368-379 of package,py with:

            init_packet = InitPacketPB(
                            from_bytes = None,
                            hash_bytes=firmware_hash,
                            hash_type=HashTypes.SHA256,
                            dfu_type=HexTypeToInitPacketFwTypemap[key],
                            is_debug=firmware_data[FirmwareKeys.INIT_PACKET_DATA][PacketField.DEBUG_MODE],
                            fw_version=firmware_data[FirmwareKeys.INIT_PACKET_DATA][PacketField.FW_VERSION],
                            hw_version=firmware_data[FirmwareKeys.INIT_PACKET_DATA][PacketField.HW_VERSION],
                            sd_size=sd_size,
                            app_size=app_size,
                            bl_size=bl_size,
                            sd_req=firmware_data[FirmwareKeys.INIT_PACKET_DATA][PacketField.REQUIRED_SOFTDEVICES_ARRAY],
                            nonce=nonce)

Replace lines 104-11 of manifest.py with:

def __init__(self,
                 is_debug=None,
                 hw_version=None,
                 fw_version=None,
                 softdevice_req=None,
                 sd_size=None,
                 bl_size=None,
                 nonce=None
                 ):

Replace lines 123-128 of manifest.py with:

self.is_debug = is_debug
        self.hw_version = hw_version
        self.fw_version = fw_version
        self.softdevice_req = softdevice_req
        self.sd_size = sd_size
        self.bl_size = bl_size
        self.nonce = nonce

Replace lines 65-77 of init_packet_pb.py with:

    def __init__(self,
                 from_bytes = None,
                 hash_bytes = None,
                 hash_type = None,
                 dfu_type = None,
                 is_debug=False,
                 fw_version=0xffffffff,
                 hw_version=0xffffffff,
                 sd_size=0,
                 app_size=0,
                 bl_size=0,
                 sd_req=None,
                 nonce = None
                 ):

Replace lines 100-110 of init_packet_pb.py with:

            self.init_command = pb.InitCommand()
            self.init_command.hash.hash_type = hash_type.value
            self.init_command.type = dfu_type.value
            self.init_command.hash.hash = hash_bytes
            self.init_command.is_debug = is_debug
            self.init_command.fw_version = fw_version
            self.init_command.hw_version = hw_version
            self.init_command.sd_req.extend(list(set(sd_req)))
            self.init_command.sd_size = sd_size
            self.init_command.bl_size = bl_size
            self.init_command.app_size = app_size
            self.init_command.nonce = nonce

https://devzone.nordicsemi.com/cfs-file/__key/support-attachments/beef5d1b77644c448dabff31668f3a47-44326a9c207d4171a49f9b8ad4cb8a67/nrfutil.exe

The following command will display the contents of the init packet and allow you to view the 12-byte nonce value:

nrfutil pkg display dfu_pkg_app_v0_0_1.zip


Bootloader changes required to process encrypted firmware

Let's begin by making the necessary changes to two files in the dfu libraries (nrf_dfu_validation.c, nrf_dfu_req_handler.c, dfu-cc.proto & dfu-cc.options) to compute the appropriate crc32 values in the correct places; to decrypt the firmware images and to correctly process the newly added nounce in the init packet.

nrf_dfu_validation.c

In nrf_dfu_validation.c we want to add the following code which includes a function for intialising the ECB module:

#define ECB_KEY_LEN         (16UL)
#define COUNTER_BYTE_LEN    (4UL)
#define NONCE_BYTE_LEN      (12UL)

static bool m_initialized = false;
static uint8_t m_index = 0;
static uint32_t m_counter = 0;
static dfu_fw_type_t m_dfu_fw_type;

// NOTE: The ECB data must be located in RAM or a HardFault will be triggered.
static nrf_ecb_hal_data_t m_ecb_data;

/**
 * @brief Initializes the module with the given nonce and key
 * @details The nonce will be copied to an internal buffer so it does not need to
 *          be retained after the function returns. Additionally, a 32-bit counter
 *          will be initialized to zero and placed into the least-significant 4 bytes
 *          of the internal buffer. The nonce value should be generated in a
 *          reasonable manner (e.g. using this module's nonce_generate function).
 *
 * @param[in]    p_nonce    An array of length 16 containing 12 random bytes
 *                          starting at index 4
 * @param[in]    p_ecb_key  An array of length 16 containing the ECB key
 */
static void ctr_init(const uint8_t * p_nonce, const uint8_t * p_ecb_key)
{
    m_initialized = true;
    m_index = 0;
    m_counter = 0;

    // Copy the nonce.
    memcpy(&m_ecb_data.cleartext[0], p_nonce, ECB_KEY_LEN);

    /* Reverse the array as ECB expects it in big-endian format */
    for (uint8_t i=0; i<ECB_KEY_LEN; i++)
    {
        // Save the key.
        m_ecb_data.key[i] = p_ecb_key[ECB_KEY_LEN-1-i];
    }
}

Here is our encrypt/decrypt code which has been modified from the version found here to allow us to access it from the on_data_obj_write_request function in nrf_dfu_req_handler.c and allow us to pass it one byte at a time. Add the following function to nrf_dfu_validation.c:

uint32_t nrf_dfu_validation_crypt(uint8_t * buf)
{
    uint32_t err_code;

    if (!m_initialized)
    {
        return NRF_ERROR_INVALID_STATE;
    }
    
    if (m_index == 0)
    {
        err_code = sd_ecb_block_encrypt(&m_ecb_data);
        if (NRF_SUCCESS != err_code)
        {
            return err_code;
        }
    }

    * buf ^= m_ecb_data.ciphertext[m_index];

    m_index++;

    if (m_index == 16)
    {
        m_index = 0;
        
        //Increment the counter
        m_counter++;

        m_ecb_data.cleartext[ECB_KEY_LEN-1] = (uint8_t)(m_counter & 0xFF);
        m_ecb_data.cleartext[ECB_KEY_LEN-2] = (uint8_t)((m_counter >> 8) & 0xFF);
        m_ecb_data.cleartext[ECB_KEY_LEN-3] = (uint8_t)((m_counter >> 16) & 0xFF);
        m_ecb_data.cleartext[ECB_KEY_LEN-4] = (uint8_t)((m_counter >> 24) & 0xFF);
    }

    return NRF_SUCCESS;
}

In order to check whether the firmware type requires decryption, add the following function to nrf_dfu_validation.c:

bool nrf_dfu_validation_crypt_required()
{
    if ((m_dfu_fw_type == DFU_FW_TYPE_APPLICATION) || (m_dfu_fw_type == DFU_FW_TYPE_BOOTLOADER))
    {
        return true;
    }
    
    return false;
}

We need to create the following function in nrf_dfu_validation.c to initialise the ECB module with the ECB key and nonce from the init packet:

static uint32_t nrf_dfu_validation_crypt_init(dfu_init_command_t const * p_init)
{
    const uint8_t ecb_key[ECB_KEY_LEN] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};//ENTER ACTUAL ECB KEY HERE (12 cryptographically random bytes)
    uint8_t nonce[16] = {0};
    
    m_dfu_fw_type = p_init->type;
    
    for (uint8_t i=0; i<NONCE_BYTE_LEN; i++)
    {
        nonce[i] = p_init->nonce.bytes[NONCE_BYTE_LEN - 1 - i];
    }
    
    ctr_init(&nonce[0], &ecb_key[0]);
    
    return NRF_SUCCESS;
}

Call the new nrf_dfu_validation_crypt_init() function from within the nrf_dfu_validation_init_cmd_execute() function as shown below:

if (m_packet.has_signed_command)
{
    p_command = &m_packet.signed_command.command;
    signature_type =  m_packet.signed_command.signature_type;
    p_signature    = &m_packet.signed_command.signature;
}

nrf_dfu_validation_crypt_init(&p_command->init);

// Validate signature.
ret_val = signature_check(p_command->init.type, signature_type, p_signature);

In the nrf_dfu_validation_post_data_execute function, add the following code to compute a crc32 value on the decrypted firmware image that has been written to flash. This crc32 value will be stored (written to flash) and checked on every subsequent startup. Because we are encrypting application firmware images and bootloader firmware images, the crc32 needs to computed in both scenarios.

if (p_init->type == DFU_FW_TYPE_APPLICATION || p_init->type & DFU_FW_TYPE_BOOTLOADER)
{
    s_dfu_settings.progress.firmware_image_crc = crc32_compute((uint8_t*)src_addr, data_len, NULL);
}

The location to place this code in the nrf_dfu_validation_post_data_execute function is shown below:

if (!fw_hash_ok(p_init, src_addr, data_len))
{
    ret_val = EXT_ERR(NRF_DFU_EXT_ERROR_VERIFICATION_FAILED);
}
else
{
    if (p_init->type == DFU_FW_TYPE_APPLICATION || p_init->type & DFU_FW_TYPE_BOOTLOADER)
    {
        s_dfu_settings.progress.firmware_image_crc = crc32_compute((uint8_t*)src_addr, data_len, NULL);
    }

    if (p_init->type == DFU_FW_TYPE_APPLICATION)
    {
        postvalidate_app(p_init);
    }
    else

nrf_dfu_req_handler.c

In the on_data_obj_write_request function, the crc32 value is currently computed after the call to nrf_dfu_flash_store. We want to compute the crc32 value on the encrypted data. Add the following line of code before the call to the nrf_dfu_flash_store function:

uint32_t crc32_compute_encrypted = crc32_compute(p_req->write.p_data, p_req->write.len, &s_dfu_settings.progress.firmware_image_crc);

Replace:

s_dfu_settings.progress.firmware_image_crc     = crc32_compute(p_req->write.p_data, p_req->write.len, &s_dfu_settings.progress.firmware_image_crc);

with the following code to set the crc32 value previously computed on the encrypted data:

s_dfu_settings.progress.firmware_image_crc     = crc32_compute_encrypted;

Now let's turn our focus to decrypting the data. In on_data_obj_write_request function in nrf_dfu_req_handler.c add the following code (prior to the call to nrf_dfu_flash_store) to loop through the buffer and decrypt each individual byte:

if (nrf_dfu_validation_crypt_required())
{
    for (int i = 0; i < p_req->write.len; i++)
    {
        nrf_dfu_validation_crypt(&p_req->write.p_data[i]);
    }
}

The code above needs to be placed after the crc32 has been computed and before the call to nrf_dfu_flash_store function (as shown below):

uint32_t crc32_compute_encrypted = crc32_compute(p_req->write.p_data, p_req->write.len, &s_dfu_settings.progress.firmware_image_crc);

if (nrf_dfu_validation_crypt_required())
{
    for (int i = 0; i < p_req->write.len; i++)
    {
        nrf_dfu_validation_crypt(&p_req->write.p_data[i]);
    }
}

ret_code_t ret = nrf_dfu_flash_store(write_addr, p_req->write.p_data, p_req->write.len, p_req->callback.write);

dfu-cc.proto & dfu-cc.options

Add the following code to the initCommand message definition found in dfu-cc.proto:

optional bytes nonce = 10;

as shown below:

// Commands data
message InitCommand {
	optional uint32	fw_version	= 1;
	optional uint32	hw_version	= 2;
	repeated uint32	sd_req		= 3 [packed = true]; // packed option is default in proto3
	optional FwType	type		= 4;

	optional uint32	sd_size		= 5;
	optional uint32	bl_size		= 6;
	optional uint32	app_size	= 7;

	optional Hash	hash		= 8;
    
    optional bool   is_debug    = 9 [default = false];
	optional bytes  nonce		= 10;	
}

Replace the contents of dfu-cc.options with the following:

dfu.Hash.hash			    max_size:32
dfu.SignedCommand.signature	max_size:64
dfu.InitCommand.sd_req		max_count:12
dfu.InitCommand.nonce		max_size:12

and then perform Step 4. as described here.


Generating an encrypted package

Now let's look at how we can generate a firmware package containing an encrypted firmware image (.bin) that our new bootloader will decrypt.

Encrypting the firmware image

First generate a package (dfu_pkg_unencrypted_app_v0_0_1.zip) using the unencrypted firmware image (application_v0_0_1.hex) using the following command (ensure you are using the modified nrfutil.exe) :

nrfutil pkg generate --application-version 1 --sd-req 0x9b --hw-version 52 --application application_v0_0_1.hex --key-file private-key.pem dfu_pkg_unencrypted_app_v0_0_1.zip

Then view the contents of the init packet and obtain the 12-byte nonce value with the following command (ensure you are using the modified nrfutil.exe):

nrfutil pkg display dfu_pkg_unencrypted_app_v0_0_1.zip

Next unzip dfu_pkg_unencrypted_app_v0_0_1.zip.

Edit the following command to replace '100f0e0d0c0b0a090807060504030201' with your ECB Key (in big-endian format), and replace the 'ffffffffffffffffffffffff' in 'ffffffffffffffffffffffff00000000' with the12-byte nonce found previously (in big-endian format).

C:\OpenSSL-Win64\bin\openssl.exe enc -e -aes-128-ctr -in dfu_pkg_unencrypted_app_v0_0_1/application_v0_0_1.bin -out dfu_pkg_unencrypted_app_v0_0_1/encrypted_application_v0_0_1.bin -K 100f0e0d0c0b0a090807060504030201 -iv ffffffffffffffffffffffff00000000 -v

The previous command will create a file called encrypted_application_v0_0_1.bin in the unzipped folder called dfu_pkg_unencrypted_app_v0_0_1. Delete the file labelled application_v0_0_1.bin and rename encrypted_application_v0_0_1.bin to application_v0_0_1.bin.

Leave both the application_v0_0_1.dat and the manifest.json files untouched and zip the three files (.bin, .dat, .json) up into 'dfu_pkg_app_v0_0_1.zip' - you now have an encrypted firmware package.

Related