Introduction

Z-Stack is a component of the SimpleLink CC13xx / CC26xx Software Development Kit and is a complete Zigbee 3.0 software solution.

The intention of this lab is to help developers use Z-Stack security features to protect centralized networks and their applications from attack. Topics that we will cover in this lab include:

  1. Using install codes
  2. Implementing a deny/allow list
  3. Updating security keys

The Zigbee 3.0 specification includes several security updates which are summarized in SWRA615. Please refer to this White Paper for security features which are not extensively covered in this lab. Other security types, such as those used by distributed networks, TouchLink, and Green Power, are outside the scope of this lab. More information regarding these modes are provided in the Z-Stack Overview section of the TI Z-Stack User's Guide.

Note: This lab is intended to be an informative guide concerning security development and may not offer the optimal solution for any given application.

Technical support

For any questions you may have, please refer to the TI Zigbee & Thread E2E Forum.

Prerequisites

Background

  • Familiarity with the Zigbee 3.0 specification
    • More information about the changes in Zigbee 3.0 compared to older specifications can be found in SWRA615
  • Basic CCS knowledge
  • Some basic familiarity with embedded programming.

Software

Hardware

Part 1: Using Install Codes

Purpose

Install codes enhance network security by avoiding APS encryption of the NWK key using the Global Trust Center Link Key (TCLK) typically required for joining devices. Using the key derived by the install code, which is shared between the Trust Center and joining device, ensures that no globally-known key is used to encrypt data over-the-air. In this part of the lab, we are going to set a shared install code between two devices to demonstrate this functionality.

Task 0: Erase the Flash Memory on your LaunchPads

Make sure that the flash memory has been completely erased on each of your LaunchPads before flashing any Zigbee projects. This can be done in Uniflash, here:

Task 1: Setting the Install Code of a Switch Router

  1. We will start by loading the out-of-box ZR (Zigbee Router) switch project. In CCS, go to File > Import > C/C++ > CCS Projects and under Select search-directory: select the following search path:

    C:\ti\simplelink_cc13xx_cc26xx_sdk_<version>\examples\rtos\<LaunchPad variant>\zstack\zr_sw

  2. Create a static uint8_t installCode[INSTALL_CODE_LEN + INSTALL_CODE_CRC_LEN] locally inside of zcl_samplesw.c, a 16-byte sequence followed by the proper 2-byte CRC. If the CRC is not pre-calculated then a default value should be provided so that it may be overwritten by an API procedure to follow. This example will use a value of {0x83,0xFE,0xD3,0x40,0x7A,0x93,0x97,0x23,0xA5,0xC6,0x39,0xB2,0x69,0x16,0xD5,0x05,0xC3,0xB5}; which produces the following APS LNK key once passed through a MMO hash function: 66B6900981E1EE3CA4206B6B861C02BB.

    /*********************************************************************
     * LOCAL VARIABLES
     */
    static uint8_t installCode[INSTALL_CODE_LEN + INSTALL_CODE_CRC_LEN] =
            {0x83,0xFE,0xD3,0x40,0x7A,0x93,0x97,0x23,0xA5,0xC6,0x39,0xB2,0x69,0x16,0xD5,0x05,0xC3,0xB5};
    

    zcl_samplesw.c

  3. Add the following code to the end of zclSampleSw_Init:

    if(BDB_DEFAULT_JOIN_USES_INSTALL_CODE_KEY == TRUE)
    {
      zstack_bdbSetActiveCentralizedLinkKeyReq_t zstack_bdbSetActiveCentralizedLinkKeyReq;
    
      memset(&zstack_bdbSetActiveCentralizedLinkKeyReq,0, sizeof(zstack_bdbSetActiveCentralizedLinkKeyReq_t));
      zstack_bdbSetActiveCentralizedLinkKeyReq.zstack_CentralizedLinkKeyModes = zstack_UseInstallCode;
      zstack_bdbSetActiveCentralizedLinkKeyReq.pKey = OsalPort_malloc(INSTALL_CODE_LEN + INSTALL_CODE_CRC_LEN);
      OsalPort_memcpy(zstack_bdbSetActiveCentralizedLinkKeyReq.pKey,installCode,INSTALL_CODE_LEN + INSTALL_CODE_CRC_LEN);
      Zstackapi_bdbSetActiveCentralizedLinkKeyReq(appServiceTaskId, &zstack_bdbSetActiveCentralizedLinkKeyReq);
    
      OsalPort_free(zstack_bdbSetActiveCentralizedLinkKeyReq.pKey);
    }
    

    zcl_samplesw.c

    The Zstackapi_bdbSetActiveCentralizedLinkKeyReq API sets the local device's centralized link key to the one generated after passing the install code and CRC through a MMO hash function. If no CRC has been provided then it must be generated by placing the following at the beginning of the "if" statement above (for the example it is already calculated as 0xB5C3):

    zstack_bdbGenerateInstallCodeCRCReq_t zstack_bdbGenerateInstallCodeCRCReq;
    zstack_bdbGenerateInstallCodeCRCRsp_t zstack_bdbGenerateInstallCodeCRCRsp;
    
    OsalPort_memcpy(zstack_bdbGenerateInstallCodeCRCReq.installCode,installCode,INSTALL_CODE_LEN);
    Zstackapi_bdbGenerateInstallCodeCRCReq(appServiceTaskId,&zstack_bdbGenerateInstallCodeCRCReq,
                                                        &zstack_bdbGenerateInstallCodeCRCRsp);
    
    installCode[INSTALL_CODE_LEN] = zstack_bdbGenerateInstallCodeCRCRsp.CRC & 0xFF;
    installCode[INSTALL_CODE_LEN + 1] = zstack_bdbGenerateInstallCodeCRCRsp.CRC >> 8;
    

    zcl_samplesw.c

  4. Set BDB_DEFAULT_JOIN_USES_INSTALL_CODE_KEY to TRUE in bdb_interface.h so that it is understood that install codes are used for joining.

  5. Debug the project inside CCS and open the Expressions and Memory Browser windows. Run the project and view the zstack_user0Cfg in the Expressions window to find the extended IEEE address of the ZR switch device. Alternatively, this value can be located in the FCFG1_BASE + EXTADDR_OFFSET area of the Memory Browser defined as FCFG1_MAC_15_4_0. The upper 32 bits represent the manufacturer-specific code (i.e. Texas Instruments) whereas the lower 32 bits are the unique device identification numbers.

You can optionally view the Device Info row of the Common User Interface, the MAC Address tab on Flash Programmer 2, or the Settings & Utilities tab of Uniflash to get the IEEE Address. This value will be used for the Trust Center in the following task.

Task 2: Setting the Install Code and Target Address of a Light Coordinator

  1. Repeat Step 1 of Task 1, except with the zc_light (Zigbee Coordinator light) project:

    C:\ti\simplelink_cc13xx_26xx_sdk_<version>\examples\rtos\<LaunchPad variant>\zstack\zc_light

  2. Repeat Step 2 of Task 1, except in zcl_samplelight.c. Also add a static uint8_t installCodeAddr[Z_EXTADDR_LEN] variable to store the IEEE Address, which we discovered while debugging the ZR Switch project, in little endian format (least significant byte first). The example uses {0xCB,0x57,0x9C,0x18,0x00,0x4B,0x12,0x00}; but will of course vary since this address is unique to a particular device.

    /*********************************************************************
     * LOCAL VARIABLES
     */
    static uint8_t installCode[INSTALL_CODE_LEN + INSTALL_CODE_CRC_LEN] =
        {0x83,0xFE,0xD3,0x40,0x7A,0x93,0x97,0x23,0xA5,0xC6,0x39,0xB2,0x69,0x16,0xD5,0x05,0xC3,0xB5};
    
    static uint8_t installCodeAddr[Z_EXTADDR_LEN] = 
        {0x02, 0x23, 0xf4, 0x14, 0x00, 0x4b, 0x12, 0x00};
    

    zcl_samplelight.c

  3. Inside the BDB_COMMISSIONING_FORMATION case of zclSampleLight_ProcessCommissioningStatus, add the following:

    if(BDB_DEFAULT_JOIN_USES_INSTALL_CODE_KEY)
    {
        zstack_bdbAddInstallCodeReq_t zstack_bdbAddInstallCodeReq;
    
        memset(&zstack_bdbAddInstallCodeReq,0,sizeof(zstack_bdbAddInstallCodeReq_t));
        OsalPort_memcpy(zstack_bdbAddInstallCodeReq.pExt, installCodeAddr, Z_EXTADDR_LEN);
        OsalPort_memcpy(zstack_bdbAddInstallCodeReq.pInstallCode, installCode, INSTALL_CODE_LEN + INSTALL_CODE_CRC_LEN);
    
        Zstackapi_bdbAddInstallCodeReq(appServiceTaskId, &zstack_bdbAddInstallCodeReq);
    }
    

    zcl_samplelight.c

    This is performed once during network formation by the Coordinator to ensure that the Trust Center adds all default install codes and extended addresses. If multiple joining devices are supported, this code block should be replicated for each unique IEEE Address and varying install codes (if desired).

  4. As before, set BDB_DEFAULT_JOIN_USES_INSTALL_CODE_KEY to TRUE in bdb_interface.h.

  5. Debug and run the ZC Light project. If you have multiple LaunchPads connected to your PC, CCS may prompt you to select one of the connected LaunchPads via the box below:

    The selected LaunchPad's serial number will be assigned to this project workspace so future program launches will automatically attempt to select this LaunchPad first if it is plugged in. More information about debugging with multiple probes can be found here.

Task 3: Commissioning and Viewing Results

  1. If using a packet sniffer (optional), the link key created by the install code must be provided to the software program as an Application or Trust Center Link Key or else the APS packets will be encrypted and illegible. If using the example from Task 1 then the key from Step 2 can be directly inserted.

  2. Once both projects are actively running, press BTN-1 on the ZC Light followed by BTN-1 on the ZR Switch to start the commissioning process. If the devices form the network and join successfully then pressing BTN-2 of the ZR Switch should be able to turn on LED-1 of the ZC Light.

  3. Inability to provide the correct install codes or IEEE Addresses will result in failure for the devices to communicate past Association, at which point the joining device will attempt BDBC_REC_SAME_NETWORK_RETRY_ATTEMPS + 1 times (defined in bdb_interface.h) before giving up.

Part 2: Implementing a Deny/Allow List

Purpose

Denylists use network parameters to filter out known unwanted devices or leave a specifically undesired network. On the other side, allowlists can ensure that only certain devices or networks are allowed. The following tasks provide an example of how these can be implemented for a Zigbee application.

Task 1: Rejecting Joining Device Extended Addresses on a Coordinator

  1. Open the previously used ZC light project, reset BDB_DEFAULT_JOIN_USES_INSTALL_CODE_KEY to FALSE in bdb_interface.h but add DENY_LIST to the Project > Properties > CCS Build > ARM Compiler > Predefined Symbols list so that the feature can easily be toggled on or off.

  2. On the previously used ZR switch project, reset BDB_DEFAULT_JOIN_USES_INSTALL_CODE_KEY to FALSE in bdb_interface.h.

  3. Define the extended address of a device that we do not want to join our network in zcl_samplelight.c. For this example, we will use the address of the ZR Switch from Part 1.

      /*********************************************************************
       * LOCAL VARIABLES
       */
      static uint8_t denyListAddr[Z_EXTADDR_LEN] = {0xCB,0x57,0x9C,0x18,0x00,0x4B,0x12,0x00};
    

    zcl_samplelight.c

  4. We will then verify that each joining device does not contain this unique identifier. This is best handled by the zstackmsg_CmdIDs_ZDO_DEVICE_ANNOUNCE callback which notifies the application whenever a device has joined the network. First, the callback must be registered inside the SetupZStackCallbacks function:

    static void SetupZStackCallbacks(void)
    {
        zstack_devZDOCBReq_t zdoCBReq = {0};
    
        // Register for Callbacks, turn on:
        //  Device State Change,
        //  ZDO Match Descriptor Response,
        zdoCBReq.has_devStateChange = true;
        zdoCBReq.devStateChange = true;
        zdoCBReq.has_matchDescRsp = true;
        zdoCBReq.matchDescRsp = true;
        zdoCBReq.has_ieeeAddrRsp = true;
        zdoCBReq.ieeeAddrRsp = true;
    #ifdef DENY_LIST
        zdoCBReq.has_deviceAnnounce = true;
        zdoCBReq.deviceAnnounce = true;
    #endif //DENY_LIST
    #if defined Z_POWER_TEST
    #if defined (POWER_TEST_POLL_DATA)
        zdoCBReq.has_deviceAnnounce = true;
        zdoCBReq.deviceAnnounce = true;
    #endif
    #endif // Z_POWER_TEST
    
        (void)Zstackapi_DevZDOCBReq(appServiceTaskId, &zdoCBReq);
    }
    

    zcl_samplelight.c

    Logic inside the zstackmsg_CmdIDs_ZDO_DEVICE_ANNOUNCE case of zclSampleLight_processZStackMsgs will process the joining device's extendedAddr, compare with denyListAddr, and form a Zstackapi_ZdoMgmtLeaveReq to send over-the-air if verified as an unwanted device. zstack_zdoMgmtLeaveReq options are set so that the joining device removes its children and does not attempt to rejoin the network.

            case zstackmsg_CmdIDs_ZDO_DEVICE_ANNOUNCE:
    #if defined (Z_POWER_TEST)
    #if defined (POWER_TEST_POLL_DATA)
            {
              zstackmsg_zdoDeviceAnnounceInd_t *pInd;
              pInd = (zstackmsg_zdoDeviceAnnounceInd_t*)pMsg;
    
              // save the short address of the ZED to send ZCL test data
              powerTestZEDAddr = pInd->req.srcAddr;
    
              // start periodic timer for sending ZCL data to zed
              OsalPortTimers_startReloadTimer(appServiceTaskId, SAMPLEAPP_POWER_TEST_ZCL_DATA_EVT, Z_POWER_TEST_DATA_TX_INTERVAL);
            }
    #endif
    #else
            {
                zstackmsg_zdoDeviceAnnounceInd_t *pInd;
                pInd = (zstackmsg_zdoDeviceAnnounceInd_t*)pMsg;
                if(OsalPort_memcmp(pInd->req.devExtAddr,denyListAddr,Z_EXTADDR_LEN) == TRUE)
                {
                    zstack_zdoMgmtLeaveReq_t zstack_zdoMgmtLeaveReq;
                    OsalPort_memcpy(zstack_zdoMgmtLeaveReq.deviceAddress, pInd->req.devExtAddr, Z_EXTADDR_LEN);
                    zstack_zdoMgmtLeaveReq.nwkAddr = pInd->req.devAddr;
                    zstack_zdoMgmtLeaveReq.options.rejoin = FALSE;
                    zstack_zdoMgmtLeaveReq.options.removeChildren = TRUE;
                    Zstackapi_ZdoMgmtLeaveReq(appServiceTaskId, &zstack_zdoMgmtLeaveReq);
                }
            }
    #endif
            break;
    

    zcl_samplelight.c

    After erasing previous flash memory, programming the device, and running the project in the CCS debugger, a joining device with the IEEE address 00:12:4B:00:14:F4:36:01 will be sent a Management Leave Request. All other devices will be allowed to join the network successfully. Multiple extended addresses can be added to the denylist and processed inside of zstackmsg_CmdIDs_ZDO_DEVICE_ANNOUNCE using a for loop.

Task 2: Filtering Available Networks on a Router

  1. Re-program the ZC LaunchPad without the denylist feature if it will interfere with this portion of the lab (erasing all flash is not needed, since the previous PANID will be used, which was stored in flash). Then open the ZR switch project, confirm BDB_DEFAULT_JOIN_USES_INSTALL_CODE_KEY is FALSE in bdb_interface.h, and add DENY_LIST to the Project > Properties > CCS Build > ARM Compiler > Predefined Symbols list.

  2. Define the Pan ID of the network that you do not want to join in zcl_samplesw.c, for example static uint16_t denyListAddr = 0x5C4F;

  3. This time we will use the zstackmsg_CmdIDs_BDB_FILTER_NWK_DESCRIPTOR_IND case of zclSampleSw_processZStackMsgs to determine if we want to join the available networks that were found based on the Network Descriptors provided.

            case zstackmsg_CmdIDs_BDB_FILTER_NWK_DESCRIPTOR_IND:
            {
    
             /*   User logic to remove networks that do not want to join
              *   Networks to be removed can be released with Zstackapi_bdbNwkDescFreeReq
              */
    #ifdef DENY_LIST
                zstackmsg_bdbFilterNwkDescriptorInd_t *pInd =
                        (zstackmsg_bdbFilterNwkDescriptorInd_t*)pMsg;
    
                zstack_bdbNwkDescFreeReq_t zstack_bdbNwkDescFreeReq;
                uint8_t i = 0;
    
                for (i = 0; i < pInd->bdbFilterNetworkDesc.count; i++)
                {
                    if (pInd->bdbFilterNetworkDesc.pBDBListNwk->panId == denyListAddr)
                    {
                        zstack_bdbNwkDescFreeReq.nodeDescToRemove = pInd->bdbFilterNetworkDesc.pBDBListNwk;
                        Zstackapi_bdbNwkDescFreeReq(appServiceTaskId, &zstack_bdbNwkDescFreeReq);
                    }
                    pInd->bdbFilterNetworkDesc.pBDBListNwk++;
                }
    #endif //DENY_LIST
    
              Zstackapi_bdbFilterNwkDescComplete(appServiceTaskId);
            }
            break;
    

    zcl_samplesw.c

    If the panId of a pBDBListNwk is matched to denyListAddr then the corresponding networkDesc_t is processed through the Zstackapi_bdbNwkDescFreeReq API to release it from the list of networks found during the discovery of suitable networks to join. Multiple descriptors can be removed before Zstackapi_bdbFilterNwkDescComplete indicates that all remaining networks found can attempt joining. An Association Request will not be sent to the denylisted networks who indicate that they are open and permitted to join. As before, this same implementation can be used as an allowlist if only the defined pan ID's are not filtered out.

Part 3: Updating Network Security Keys

Default network keys are determined by the DEFAULT_KEY macro in zstack_config.h, where an initial value of {0} allows for a random assignment. But some applications may desire to further implement security policies by updating the network key, for example at regular periodic intervals as controlled by the Trust Center. This can be accomplished through use of the Zstackapi_secNwkKey* APIs. The example below uses the ZC Light project to update the NWK key of all devices on the network when the LaunchPad's BTN-2 button is pressed. It first reads its own active key with Zstackapi_secNwkKeyGetReq (solely for debugging purposes) and then sets a random alternate key through Zstackapi_secNwkKeySetReq. After broadcasting a Zstackapi_secNwkKeyUpdateReq to update all network device's alternate key, Zstackapi_secNwkKeySwitchReq will send an over-the-air message commanding them to use this alternate key as the active key. All NWK keys used are stored and maintained in the Trust Center.

Warning

Be sure to clean and rebuild both projects with DENY_LIST disabled and BDB_DEFAULT_JOIN_USES_INSTALL_CODE_KEY set to FALSE.

//Button 2
if(key == CONFIG_BTN_RIGHT)
{
    static uint8_t seqNum = 0;
    zstack_secNwkKeyGetReq_t zstack_secNwkKeyGetReq;
    zstack_secNwkKeyGetRsp_t zstack_secNwkKeyGetRsp;
    zstack_secNwkKeySetReq_t zstack_secNwkKeySetReq;
    zstack_secNwkKeyUpdateReq_t zstack_secNwkKeyUpdateReq;
    zstack_secNwkKeySwitchReq_t zstack_secNwkKeySwitchReq;

    seqNum++;

    zstack_secNwkKeyGetReq.activeKey = TRUE;
    Zstackapi_secNwkKeyGetReq(appServiceTaskId, &zstack_secNwkKeyGetReq, &zstack_secNwkKeyGetRsp);

    zstack_secNwkKeySetReq.activeKey = FALSE;
    zstack_secNwkKeySetReq.seqNum = seqNum;
    zstack_secNwkKeySetReq.has_key = FALSE;
    Zstackapi_secNwkKeySetReq(appServiceTaskId, &zstack_secNwkKeySetReq);

    zstack_secNwkKeyUpdateReq.dstAddr = 0xFFFF;
    zstack_secNwkKeyUpdateReq.seqNum = seqNum;
    Zstackapi_secNwkKeyUpdateReq(appServiceTaskId, &zstack_secNwkKeyUpdateReq);

    zstack_secNwkKeySwitchReq.dstAddr = 0xFFFF;
    zstack_secNwkKeySwitchReq.seqNum = seqNum;
    Zstackapi_secNwkKeySwitchReq(appServiceTaskId, &zstack_secNwkKeySwitchReq);
}

zclSampleLight_processKey in zcl_samplelight.c

Communication afterwards will continue like normal but using the new network key. Performing the update will also reset the network frame counter, which is mandated to be persistent across factory new resets as a security means towards preventing replay attacks. Sniffer devices which were active during this process will store the new keys, but all other devices will be unable to decipher any NWK-encrypted packets that follow. The example uses a random key through zstack_secNwkKeySetReq.has_key = FALSE;, however a custom key can be used by setting this option to TRUE and populating the key field with a 16-byte value. Specific devices can solely have their NWK keys affected by modifying dstAddr with the targeted network address.

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.