前言
本文先简单介绍OOB配对的流程,然后结合CC2652蓝牙芯片调试OOB配对
一、OOB是啥?
OOB就是Out of Band的缩写,指安全数据不经过自己通信的信道进行传输。OOB允许两个蓝牙设备通过设备的带外通道发送认证信息,比如:串口、NFC、UWB等。OOB的目的是允许两个设备在没有输入输出能力的情况下创建认证配对,认证需要发送3个认证参数:device address、random number、confirm 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。
-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数据准备好了之后再走配对流程。
-1- 先把L2CAP Coc通信打通,我们需要做四件事:
双方都把L2CAP Connection Oriented Channel…勾选上
对于Central,需要把配对模式改成不允许配对,这样在建立连接之后就不会立即配对了。
下面是两个设备注册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配对主要是获取双方的3个参数,BDAddr,Confirm值,rand值,它比其它的配对(数值比较,Passkey Entry)简单,无需IO能力。
在调试OOB的时候注意配对请求发起端的配对发起条件,需要明白在什么时候才发起配对。
本文只是记录我调试OOB配对绑定的过程,如果有错误的地方,希望大伙们可以提出来,Thx~