前言

  本文先简单介绍OOB配对的流程,然后结合CC2652蓝牙芯片调试OOB配对

一、OOB是啥?

  OOB就是Out of Band的缩写,指安全数据不经过自己通信的信道进行传输。OOB允许两个蓝牙设备通过设备的带外通道发送认证信息,比如:串口、NFC、UWB等。OOB的目的是允许两个设备在没有输入输出能力的情况下创建认证配对,认证需要发送3个认证参数:device addressrandom numberconfirm value。它这种认证配对方式能很好的反MITM攻击。方法就是在主机或者从机中产生一个密钥,然后OOB数据通过蓝牙通信之外的方式发送给其他设备。OOB数据可以选择使用椭圆曲线加密算法生成(也可以不使用)。流程大致是这样的:
  1.创建存放OOB数据的变量,有两个,一个是用于存放设备自己本身的OOB数据,另一个是用于存放对端设备的OOB数据。
  2.定义配对状态的回调函数。
  3.要用ECC密钥产生OOB数据,先调用GAPBondMgr_GenerateEccKeys()产生ECC密钥,然后在ECC密钥产生的事件中再调用GAPBondMgr_SCGetLocalOOBParameters()获取OOB数据。当协议栈的ECC密钥准备好了之后,协议栈会发送事件GAPBOND_GENERATE_ECC_DONE给应用层,该事件在配对状态回调里面处理。上面提到的两个API函数可以在蓝牙连接建立前或者后调用,但是必须是在配对开始前被调用。如果不调用GAPBondMgr_GenerateEccKeys(),那么OOB数据是没使用ECC密钥生成的。
  4.在OOB数据生成之后,设备应该将它发送到对端设备,对端设备的配对特性必须是使能了OOB配对绑定的。如果两个设备都产生和分享OOB数据,那么这两个设备的配对特性都必须使能OOB配对绑定。
  5.对端设备接收到OOB数据(Confirm值和随机数)之后,应使用函数接口GAPBondMgr_SCSetRemoteOOBParameters()记录下接收到的OOB数据到绑定管理器中。
  6.设备发起配对请求,走配对流程,整个过程两设备不存在输入输出显示操作,配对完成之后两设备BLE协议栈会向应用层发出事件GAPBOND_PAIRING_STATE_COMPLETE

二、OOB配对实践

  下面我们先做两个实验:
  (1)一个设备产生OOB数据,另一个设备不产生OOB数据,实现OOB配对。
  (2)两个设备都产生OOB数据,实现OOB配对。
  在(1)中,做法可以是这样:
  在IAR中导入CC26XX的SimpleCentral例程(不要在CCS导入,因为调用ECC密钥产生会报错,修改比较麻烦)。
-1- 先定义两个变量:

#ifdef USING_OOB_PAIR
// This is needed for the device that generates OOB data
gapBondOOBData_t localOobData;
// This is needed to store the OOB data this device receives
gapBondOOBData_t remoteOobData;
#endif

-2- 在GAP_DEVICE_INIT_DONE_EVENT事件中添加ECC密钥产生函数,该函数调用成功之后会产生一个事件:GAPBOND_GENERATE_ECC_DONE

GAPBondMgr_GenerateEccKeys();

-3- 在SimpleCentral_processPairState函数里面添加一个事件:

static void SimpleCentral_processPairState(uint8_t state,
                                           scPairStateData_t* pPairData)
{
  uint8_t status = pPairData->status;
  uint8_t pairMode = 0;
  if (state == GAPBOND_GENERATE_ECC_DONE)
  {
    if (status == SUCCESS)
    {
       //After we get the ECC key, we can get the OOB data which is generated by ECC
       GAPBondMgr_SCGetLocalOOBParameters(&localOobData);
       uint8_t i;
       for (i = 0; i < KEYLEN; i++)
       {
         Display_printf(dispHandle, SC_ROW_CUR_CONN+i+5, 0, "OOB data confirm[%d]: 0x%2x", i, localOobData.confirm[i]);
         Display_printf(dispHandle, SC_ROW_CUR_CONN+KEYLEN+i+5, 0, "OOB data rand[%d]: 0x%2x", i, localOobData.rand[i]);
       }
    }
  }
  else if (state == GAPBOND_PAIRING_STATE_STARTED)
  ...
}

-4- 编译运行Central例程,使用串口助手(我使用的是Xshell)查看Central打印出的OOB数据,包括了Confirm值和随机数。
-5- 至此,Central例程的代码已经改完了,现在改从机代码,从机代码我使用的是ProjectZero,同样,建一个变量:

#ifdef USING_OOB_PAIR
// This is needed for the device that generates OOB data
gapBondOOBData_t localOobData;
// This is needed to store the OOB data this device receives
gapBondOOBData_t remoteOobData;//we'll use
#endif

-6- 直接将Central串口打印出来的Confirm值和随机数赋值到remoteOobData,然后存储到配对管理器中:

#ifdef USING_OOB_PAIR
void oob_ready(void)
{
  uint8_t u8OOB_Enable = true;
  GAPBondMgr_SetParameter(GAPBOND_OOB_ENABLED, sizeof(uint8_t), &u8OOB_Enable);
  remoteOobData.confirm[0]  = 0xe0;
  remoteOobData.confirm[1]  = 0xa6;
  remoteOobData.confirm[2]  = 0xb5;
  remoteOobData.confirm[3]  = 0x76;
  remoteOobData.confirm[4]  = 0x3d;
  remoteOobData.confirm[5]  = 0x30;
  remoteOobData.confirm[6]  = 0xe6;
  remoteOobData.confirm[7]  = 0xa1;
  remoteOobData.confirm[8]  = 0x9f;
  remoteOobData.confirm[9]  = 0xa1;
  remoteOobData.confirm[10] = 0x9f;
  remoteOobData.confirm[11] = 0x89;
  remoteOobData.confirm[12] = 0xb0;
  remoteOobData.confirm[13] = 0xa2;
  remoteOobData.confirm[14] = 0x1f;
  remoteOobData.confirm[15] = 0x94;
  remoteOobData.rand[0]  = 0xec;
  remoteOobData.rand[1]  = 0x04;
  remoteOobData.rand[2]  = 0xf8;
  remoteOobData.rand[3]  = 0xa4;
  remoteOobData.rand[4]  = 0xd8;
  remoteOobData.rand[5]  = 0x69;
  remoteOobData.rand[6]  = 0x39;
  remoteOobData.rand[7]  = 0x94;
  remoteOobData.rand[8]  = 0xce;
  remoteOobData.rand[9]  = 0xf9;
  remoteOobData.rand[10] = 0xf0;
  remoteOobData.rand[11] = 0x1d;
  remoteOobData.rand[12] = 0x1e;
  remoteOobData.rand[13] = 0xfb;
  remoteOobData.rand[14] = 0x45;
  remoteOobData.rand[15] = 0x1c;
  GAPBondMgr_SCSetRemoteOOBParameters(&remoteOobData, 1);
}
#endif

-7- 在ProjectZero的GAP_DEVICE_INIT_DONE_EVENT事件之中添加该函数

static void ProjectZero_processGapMessage(gapEventHdr_t *pMsg)
{
    ...
    case GAP_DEVICE_INIT_DONE_EVENT:
    {
        ...
        oob_ready();
        break;
...

-8- 注意,这个期间,Central不要复位,复位后OOB数据就不一样啦。编译运行ProjectZero,这里建议把ProjectZero的蓝牙地址模式设置成Public,方便Central那边扫描连接。使用Central连接ProjectZero,如果不知道怎么操作的话,可以看下工程里边的Readme。
OOB配对原理及应用-LMLPHP
-9- 扫描到从机设备之后connect to它,由于Central是配对请求发起端,在建立连接之后马上发起配对,这个时候我们可以在Central的串口助手上看到“Pairing success”和“Bond save success”信息,表示OOB配对成功。

-10- 断开连接,重新扫描连接,你可以看到“Encryption success”信息,表示之后的连接都是加密的,也从侧面表示配对成功了。

  上面介绍了一个设备产生OOB数据,另一个设备不产生OOB数据实现OOB配对,下面就继续介绍双方都产生OOB数据实现配对应该怎么做。这里先用SmartRF flash program擦除下两个芯片,因为我们前面已经实现配对绑定了,需要擦除绑定信息,好让我们做第二个实验测试。
  在(2)中,我们需要考虑两个问题,从机怎么把自己的OOB数据发送给主机呢?主机需要在什么时候才发起配对请求?第一个问题,在这里我使用最简单的方法,就是L2CAP传输OOB数据,虽然也是带内,不过没关系,只要把数据传过去就行了。第二个问题,我们肯定是让OOB数据准备好了之后再走配对流程。
OOB配对原理及应用-LMLPHP

-1- 先把L2CAP Coc通信打通,我们需要做四件事:
OOB配对原理及应用-LMLPHP
  双方都把L2CAP Connection Oriented Channel…勾选上
OOB配对原理及应用-LMLPHP
  对于Central,需要把配对模式改成不允许配对,这样在建立连接之后就不会立即配对了。
OOB配对原理及应用-LMLPHP
  下面是两个设备注册SPSM的代码段:

#define APP_SPSM   0x0080
void L2CAP_vidConfig_Init( void )
{
  l2capPsm_t     psm;
  l2capPsmInfo_t psmInfo;
  if(L2CAP_PsmInfo(APP_SPSM, &psmInfo) == INVALIDPARAMETER)
  {
    psm.initPeerCredits = 0xFFFF;
    psm.maxNumChannels = MAX_NUM_BLE_CONNS;
    psm.mtu = MAX_PDU_SIZE;
    psm.peerCreditThreshold = 0;
    psm.pfnVerifySecCB = NULL;
    psm.psm = APP_SPSM;
    psm.taskId = ICall_getLocalMsgEntityId(ICALL_SERVICE_CLASS_BLE_MSG, selfEntity);
    
    L2CAP_RegisterPsm(&psm);
  }
  else
  {
    //Failed
  }
}

-1.1- 在Central一方的建立连接事件里添加L2CAP连接请求

// Send out L2CAP_LE_CREDIT_BASED_CONNECTION_REQ
L2CAP_ConnectReq(connHandle, App_SPSM, App_SPSM);

  两个设备都可以发送L2CAP Coc建立连接请求,但是不需要两个设备都发起。
-1.2- 在Central一方添加下面的代码段,"…"是我省略了一些例程本来就有的代码。

#if defined(USING_L2CAP)
// 在原有的connRec_t类型里添加cocCID
// Connected device information
typedef struct
{
  uint16_t connHandle;        // Connection Handle
  uint16_t charHandle;        // Characteristic Handle
  uint8_t  addr[B_ADDR_LEN];  // Peer Device Address
  Clock_Struct *pRssiClock;   // pointer to clock struct
  uint16_t cocCID;            // CID for the L2CAP channel
} connRec_t;
static void L2CAP_vidConfig_Init( void );
static bStatus_t Application_sendL2capData(uint8_t connHandle, uint16_t datalen, uint8_t *pdata);
static void Application_processL2CAPDataEvent(l2capDataEvent_t *pMsg);
static void Application_processL2CAPSignalEvent(l2capSignalEvent_t *pMsg);
#endif

static uint8_t SimpleCentral_processStackMsg(ICall_Hdr *pMsg)
{
  switch (pMsg->event)
  {
    ...
    case L2CAP_SIGNAL_EVENT:
      // place holder for L2CAP Connection Parameter Reply
      Application_processL2CAPSignalEvent((l2capSignalEvent_t *)pMsg);
      break;
    case L2CAP_DATA_EVENT:
      //L2CAP接收数据处理
      Application_processL2CAPDataEvent((l2capDataEvent_t *)pMsg);
      break;
    default:
      break;
  }
  ...
}

#if defined(USING_L2CAP)
#define APP_SPSM   0x0080
void L2CAP_vidConfig_Init( void )//在GAP_DEVICE_INIT_DONE_EVENT事件里面调用
{
  l2capPsm_t     psm;
  l2capPsmInfo_t psmInfo;
  if(L2CAP_PsmInfo(APP_SPSM, &psmInfo) == INVALIDPARAMETER)
  {
    psm.initPeerCredits = 0xFFFF;
    psm.maxNumChannels = MAX_NUM_BLE_CONNS;
    psm.mtu = MAX_PDU_SIZE;
    psm.peerCreditThreshold = 0;
    psm.pfnVerifySecCB = NULL;
    psm.psm = APP_SPSM;
    psm.taskId = ICall_getLocalMsgEntityId(ICALL_SERVICE_CLASS_BLE_MSG, selfEntity);
    
    L2CAP_RegisterPsm(&psm);
  }
  else
  {
    //Failed
  }
}

bStatus_t Application_sendL2capData(uint8_t connHandle, uint16_t datalen, uint8_t *pdata)
{
  l2capPacket_t pkt;
  bStatus_t status = SUCCESS;
  uint8_t connIndex;
  
  if(datalen != 0)
  {
    connIndex = SimpleCentral_getConnIndex(connHandle);
    pkt.CID = connList[connIndex].cocCID;
    pkt.pPayload = pdata;
    pkt.len = datalen;
    status = L2CAP_SendSDU(&pkt);
  }
  else
  {
    /*not need to do*/
    status = FAILURE;
  }

  return (status);
}

/******************************************************************************
* @fn          Application_processL2CAPDataEvent
*
* @brief       This function is used to handle the L2CAP data extraction.
*
* @param       pMsg - pointer to the signal that was received
*
* @return      None.
*/
#define OOB_DATA  0
void Application_processL2CAPDataEvent(l2capDataEvent_t *pMsg)
{
    if (!pMsg)
    {
      // Caller needs to figure out by himself that pMsg is NULL
      return;
    }

    // The data locates under pMsg->pkt.pPayload
    // Extract the data and do what you want to do
    switch(pMsg->pkt.pPayload[0])
    {
    case OOB_DATA:
      memcpy(remoteOobData.confirm, &(pMsg->pkt.pPayload[1]), KEYLEN);
      memcpy(remoteOobData.rand, &(pMsg->pkt.pPayload[16+1]), KEYLEN);
      GAPBondMgr_SCSetRemoteOOBParameters(&remoteOobData, 1);
      uint8_t oobEnabled = TRUE;
      GAPBondMgr_SetParameter(GAPBOND_OOB_ENABLED, sizeof(uint8_t), &oobEnabled);
      uint8_t pairMode = GAPBOND_PAIRING_MODE_INITIATE;
      GAPBondMgr_SetParameter(GAPBOND_PAIRING_MODE, sizeof(uint8_t), &pairMode);
      GAPBondMgr_Pair(pMsg->connHandle);
      break;
    default:
      break;
    }
    // Free the payload (must use BM_free here)
    BM_free(pMsg->pkt.pPayload);
}

/******************************************************************************
* @fn          Application_processL2CAPSignalEvent
*
* @brief       This function is used to handle all the L2CAP signal events.
*
* @param       pMsg - pointer to the signal that was received
*
* @return      None.
*/
static void Application_processL2CAPSignalEvent(l2capSignalEvent_t *pMsg)
{
  uint8_t SendOOBData_BUFF[33] = {(uint8_t)OOB_DATA};//1st byte is 0, and others are OOBDATA
  if (!pMsg)
  {
    return;
  }

  switch (pMsg->opcode)
  {
    case L2CAP_CHANNEL_ESTABLISHED_EVT:
    {
      l2capChannelEstEvt_t *pEstEvt = &(pMsg->cmd.channelEstEvt);

      if (pMsg->connHandle != LINKDB_CONNHANDLE_INVALID && pMsg->connHandle < MAX_NUM_BLE_CONNS)
      {
        // Successfully establish link over L2CAP
        // Extract te CIO and store in the application layer
        // This will be useful when sending data over L2CAP channels
        connList[pMsg->connHandle].cocCID = pEstEvt->CID;
        
        // Give max credits to the other side
        L2CAP_FlowCtrlCredit(pEstEvt->CID, 0xFFFF);
        //make sure you have got the local OOB data from GAPBOND_GENERATE_ECC_DONE event
        memcpy(&SendOOBData_BUFF[1], (uint8_t *)&localOobData.confirm[0], 32);
        Application_sendL2capData((uint8_t)pMsg->connHandle, 33, SendOOBData_BUFF);
      }
      else
      {
        // Could not establish an L2CAP link
      }
    }
    break;

    case L2CAP_SEND_SDU_DONE_EVT:
    {
      if (pMsg->hdr.status == SUCCESS)
      {
       // Successfully sending data over L2CAP
      }
      else
      {
      }
    }
    break;

    case L2CAP_CHANNEL_TERMINATED_EVT:
    {
    }
    break;
  }
}
#endif

-2- 在ProjectZero一方,同样添加L2CAP和OOB的相关代码:

void L2CAP_vidConfig_Init( void )//在GAP_DEVICE_INIT_DONE_EVENT事件里面调用
{
  l2capPsm_t     psm;
  l2capPsmInfo_t psmInfo;
  if(L2CAP_PsmInfo(APP_SPSM, &psmInfo) == INVALIDPARAMETER)
  {
    psm.initPeerCredits = 0xFFFF;
    psm.maxNumChannels = MAX_NUM_BLE_CONNS;
    psm.mtu = MAX_PDU_SIZE;
    psm.peerCreditThreshold = 0;
    psm.pfnVerifySecCB = NULL;
    psm.psm = APP_SPSM;
    psm.taskId = ICall_getLocalMsgEntityId(ICALL_SERVICE_CLASS_BLE_MSG, selfEntity);
    
    L2CAP_RegisterPsm(&psm);
  }
  else
  {
    //Failed
  }
}

/******************************************************************************
* @fn          Application_processL2CAPSignalEvent
*
* @brief       This function is used to handle all the L2CAP signal events.
*
* @param       pMsg - pointer to the signal that was received
*
* @return      None.
*/
void Application_processL2CAPSignalEvent(l2capSignalEvent_t *pMsg)
{
  // Sanity check
  if (!pMsg)
  {
    return;
  }

  switch (pMsg->opcode)
  {
    case L2CAP_CHANNEL_ESTABLISHED_EVT:
    {
      l2capChannelEstEvt_t *pEstEvt = &(pMsg->cmd.channelEstEvt);

      if (pMsg->connHandle != LINKDB_CONNHANDLE_INVALID && pMsg->connHandle < MAX_NUM_BLE_CONNS)
      {
        // Successfully establish link over L2CAP
        // Extract te CIO and store in the application layer
        // This will be useful when sending data over L2CAP channels
        connList[pMsg->connHandle].cocCID = pEstEvt->CID;
        
        // Give max credits to the other side
        L2CAP_FlowCtrlCredit(pEstEvt->CID, 0xFFFF);
      }
      else
      {
        // Could not establish an L2CAP link
      }
    }
    break;

    case L2CAP_SEND_SDU_DONE_EVT:
    {
      if (pMsg->hdr.status == SUCCESS)
      {
       // Successfully sending data over L2CAP
      }
      else
      {
      }
    }
    break;

    case L2CAP_CHANNEL_TERMINATED_EVT:
    {
    }
    break;
  }
}

bStatus_t Application_sendL2capData(uint8_t connHandle, uint16_t datalen, uint8_t *pdata)
{
  l2capPacket_t pkt;
  bStatus_t status = SUCCESS;
  uint8_t connIndex;
  
  if(datalen != 0)
  {
    connIndex = ProjectZero_getConnIndex(connHandle);
    pkt.CID = connList[connIndex].cocCID;
    pkt.pPayload = pdata;
    pkt.len = datalen;
    status = L2CAP_SendSDU(&pkt);
  }
  else
  {
    /*not need to do*/
    status = FAILURE;
  }

  return (status);
}

/******************************************************************************
* @fn          Application_processL2CAPDataEvent
*
* @brief       This function is used to handle the L2CAP data extraction.
*
* @param       pMsg - pointer to the signal that was received
*
* @return      None.
*/
#define OOB_DATA  0
void Application_processL2CAPDataEvent(l2capDataEvent_t *pMsg)
{
    uint8_t SendOOBData_BUFF[33] = {0};//1st byte is 0, and others are OOBDATA
    if (!pMsg)
    {
      // Caller needs to figure out by himself that pMsg is NULL
      return;
    }

    // The data locates under pMsg->pkt.pPayload
    // Extract the data and do what you want to do
    switch(pMsg->pkt.pPayload[0])
    {
    case OOB_DATA:
      uint8_t u8OOB_Enable            =    true;
      GAPBondMgr_SetParameter(GAPBOND_OOB_ENABLED, sizeof(uint8_t), &u8OOB_Enable);
      memcpy(remoteOobData.confirm, &(pMsg->pkt.pPayload[1]), KEYLEN);
      memcpy(remoteOobData.rand, &(pMsg->pkt.pPayload[16+1]), KEYLEN);
      GAPBondMgr_SCSetRemoteOOBParameters(&remoteOobData, 1);
      GAPBondMgr_SCGetLocalOOBParameters(&localOobData);
      memcpy(&SendOOBData_BUFF[1], (uint8_t *)&localOobData.confirm[0], 32);
      Application_sendL2capData(pMsg->connHandle, 33, SendOOBData_BUFF);
      break;
    default:
      break;
    }
    // Free the payload (must use BM_free here)
    BM_free(pMsg->pkt.pPayload);
}

static void ProjectZero_taskFxn(UArg a0, UArg a1)
{
	...
	 case HCI_GAP_EVENT_EVENT:
          ProjectZero_processHCIMsg(pMsg);
          break;
     case L2CAP_SIGNAL_EVENT:
          Application_processL2CAPSignalEvent((l2capSignalEvent_t *)pMsg);
          break;
     case L2CAP_DATA_EVENT:
          Application_processL2CAPDataEvent((l2capDataEvent_t *)pMsg);
          break;
     default:
          // do nothing
          break;
	...
}

-3- 两个工程编译没报错之后分别下载到两个板子上,开始测试OOB配对,主机连上从机之后,待L2CAP建立连接之后,主机将自己的OOB数据通过L2CAP发送到从机,从机接收到主机OOB数据之后将自己的OOB数据通过L2CAP发给主机,主机接收到从机OOB数据之后发起配对。结果如下:

OOB配对原理及应用-LMLPHP

总结

  OOB配对主要是获取双方的3个参数,BDAddr,Confirm值,rand值,它比其它的配对(数值比较,Passkey Entry)简单,无需IO能力。
  在调试OOB的时候注意配对请求发起端的配对发起条件,需要明白在什么时候才发起配对。
  本文只是记录我调试OOB配对绑定的过程,如果有错误的地方,希望大伙们可以提出来,Thx~

05-21 21:28