随着行业的技术发展和需求,车载OBD产品中有部分会增加蓝牙BLE和手机APP联系。兼于总体成本和成熟度的综合因素,目前市面上的OBD产品大都使用Microchip的PIC18F25K80。
基本框图如下所示:
PIC18F25K80是Microchip高性能的8位MCU,内置了ECAN模块,支持超低功耗模式。集成了常用的UART, SPI, I2C等接口,用于和外围器件通信。
详见如下链接:
https://www.microchip.com/wwwproducts/en/PIC18F25K80
由于目前国内对知识产权保护方面仍然不够完善,各个公司之间都是互相借鉴甚至直接克隆软硬件,使真正投入资源开发这类产品的公司遭受了巨大的损失。
我们知道,单纯依靠MCU本身的Flash锁定位来防止别人读出Flash中的内容已不足以防止别人的抄袭,此时只能依靠外部增加硬件的安全芯片来保护自己的设计不被非法克隆。
Microchip的ATSHA204A可以满足些类需求,详细资料可以在下面的链接中找到。
https://www.microchip.com/wwwproducts/en/ATSHA204A
ATSHA204A是支持硬件保护的安全芯片,能提供安全的存贮区域来保护用户的密钥和其它重要数据,并且支持SHA256的硬件加速引擎来完成身份验证等功能。是实现软硬件防克隆,IP保护,配件耗材管控的理想方案。
为了方便客户的硬件设计,ATSHA204A提供了丰富的封装选项,从最通用的SOIC8到最小尺寸的UDFN8(2*3mm)。
UDFN8, SOIC8, TSSOP8, SOT323, 3 PADS contact
和MCU的接口支持通用的I2C接口或单总线接口,占用MCU的脚位非常少。
ATSHA204A支持很宽的工作电压范围,2.0V~5.5V,和MCU的接口电压从1.8V到5.5V。可以非常方便地适配到大部分的系统中。
ATSHA204A内部有16个Slot可以存放数据,每个Slot都可以根据需要做不同的配置。由于这里只是演示简单的身份验证过程,只需要用一个Slot来保存密钥。可以使用ACES和CK101来对204做Provision,Slot 0用来保存密钥,所以需要设置成不可读,不可写。配置如下,
其它的Slot可以配置成可读可写的,这样就可以当作E2PROM来保存用户数据。
把配置区锁定后,就可以向Slot0中写入密钥。之后再把数据区锁定。
只有配置和数据区都锁定以后才能正常执行验证的完整过程。
AN_8834 - ATSHA204 Authentication Modes 中可以了解使用SHA256做身份验证的基本过程
上图是可以把Alice理解成MCU,Bob理解成外加的ATSHA204A安全芯片。
预先会在SHA204A中写入密钥,如上图中的Shared Key;
在现场验证时,MCU会产生一个随机数,将随机数发给ATSHA204A做Crypto Hash运算(实际上就是对密钥和随机数来做了SHA256的运算,产生一个HASH值);
同时MCU也会用代码做同样的运算(对密钥和随机数执行软件的SHA256运算,也会产生一个HASH值)
MCU等待一段时间后读取SHA204A的运算结果,和自己的结果做对比;
如果一致就可以认为外部的SHA204A是合法的,程序可以正常运行。否则就不运行。
所以,这里有两点需留意:
由于MCU中有保存密钥,所以密钥最好不要以明文形式存在,可以打散放置或做一些转化,如用位或非操作等。在需要验证前现场恢复密钥。
如果MCU中没有好的随机数生成器,可以用SHA204A来生成随机数Rm。为防止重复质询攻击,MCU需要对随机数Rm做一些处理,增加一些随机因子。比如用ADC来采样的数据替换Rm中的某些字节。再做一次SHA256运算得到Hm,用结果Hm来发起质询验证。
客户这个案子使用的是SOT23封装的单总线接口的ATSHA204A, 所以大部分的工作都是在实现MCU和SHA204A的单总线通信。
单总线的时序需求非常严格,逻辑位0和逻辑位1是通过不同的脉冲来区分的。如果开发过红外遥控的编解码程序应该对这种不会陌生。
MCU中实现单总线有两种方式:
MCU使用4MHz外部晶振,通过PLL倍频到16MHz。为了提高检测脉冲宽度的精度,使用TMR1来对SWI的脉宽测量,最小宽度63nS, 最大测试宽度为4ms,可以满足要求。
和时序相关的延时函数如下,都是通过等待TMR1的超时中断位来实现的:
void delay(uint16_t delay)
{
// Clearing IF flag.
PIR1bits.TMR1IF = 0;
TMR1H = (delay >> 8);
TMR1L = delay & 0xFF;
T1CONbits.TMR1ON = 1;
while(!PIR1bits.TMR1IF);
PIR1bits.TMR1IF = 0;
T1CONbits.TMR1ON = 0;
}
void delay_ms(uint16_t ms)
{
while(ms --)
{
// Clearing IF flag.
PIR1bits.TMR1IF = 0;
TMR1H = 0xC1;
TMR1L = 0xA0;
T1CONbits.TMR1ON = 1;
while(!PIR1bits.TMR1IF);
}
T1CONbits.TMR1ON = 0;
PIR1bits.TMR1IF = 0;
}
204A在每次操作之前都必须唤醒Wake,具体做法是将数据线拉低超过60uS即可,直接调用delay来实现。由于TMR1为累加到0xFFFF时就会产生溢出中断,所以实际延时周期是0xFFFF-X:
void swi_wake_token()
{
SDA_OUTPUT();
SDA_LOW();
delay(0xFFFF - 1000);
SDA_HIGH();
}
由于时序的要求,发送数据中途不能被中断打扰,所以必须关掉全局中断。MCU发送数据串的函数如下:
void swi_send_bytes(uint8_t count, uint8_t *buffer)
{
uint8_t i, bit_mask;
INTERRUPT_GlobalInterruptDisable();
for(i = 0; i < count; i++)
{
for(bit_mask = 1; bit_mask > 0; bit_mask <<= 1)
{
if(bit_mask & buffer[i]) //!< Send Logic 1 (7F)
{
SDA_LOW();
delay(0xFFFF - 2);
SDA_HIGH();
delay(0xFFFF - 410); //310
}
else //!< Send Logic 0 (7D)
{
SDA_LOW();
delay(0xFFFF - 2);
SDA_HIGH();
delay(0xFFFF - 2);
SDA_LOW();
delay(0xFFFF - 2);
SDA_HIGH();
delay(0xFFFF - 310); //225
}
}
}
INTERRUPT_GlobalInterruptEnable();
}
MCU接收部分就比较复杂,每检测到SDA脚上有电平变化时就开始TMR1计时,等到下次变化时对计时做出有效性判断。为了提高容错性,可以对判断的范围适当放宽一点。如果产生了超时中断,则说明收到逻辑1或者超时出错了:
ATCA_STATUS swi_receive_bytes(uint8_t *buffer, uint8_t length)
{
ATCA_STATUS status = ATCA_SUCCESS;
uint8_t i;
uint8_t bit_mask;
uint8_t pulse_count;
uint16_t pulse_length;
SDA_INPUT();
INTERRUPT_GlobalInterruptDisable();
PIR1bits.TMR1IF = 0;
TMR1H = 0;
TMR1L = 0;
T1CONbits.TMR1ON = 1;
//! Receive bits and store in buffer.
for(i = 0; i < length; i ++)
{
buffer[i] = 0;
for(bit_mask = 1; bit_mask > 0; bit_mask <<= 1)
{
pulse_count = 0;
//! Wait for falling edge.
while(SDA_STATE() && !TMR1IF);
TMR1H = 0;
TMR1L = 0;
if(PIR1bits.TMR1IF)
{
PIR1bits.TMR1IF = 0;
status = ATCA_RX_TIMEOUT;
break;
}
//! Wait for rising edge.
while(!SDA_STATE() && !PIR1bits.TMR1IF);
pulse_length = ((uint16_t)TMR1H << 8) | TMR1L;
TMR1H = 0xFF;
TMR1L = (0xFF-0x90);
if(PIR1bits.TMR1IF)
{
PIR1bits.TMR1IF = 0;
status = ATCA_RX_TIMEOUT;
break;
}
if( pulse_length > (0x70-0x18) && pulse_length < (0x70+0x18))
{
pulse_count = 1;
}
//! Wait for falling edge.
while(SDA_STATE() && !PIR1bits.TMR1IF);
pulse_length = ((uint16_t)TMR1H << 8) | TMR1L;
TMR1H = 0;
TMR1L = 0;
if(PIR1bits.TMR1IF)
{
PIR1bits.TMR1IF = 0;
if(pulse_count == 1)
//! received "one" bit
buffer[i] |= bit_mask;
}
else
{
if( pulse_length > (0xFFC5-0x18) && pulse_length < (0xFFC5+0x18))
{
//! Wait for rising edge.
while(!SDA_STATE() && !PIR1bits.TMR1IF);
pulse_length = ((uint16_t)TMR1H << 8) | TMR1L;
TMR1H = 0;
TMR1L = 0;
if(PIR1bits.TMR1IF)
{
PIR1bits.TMR1IF = 0;
status = ATCA_RX_TIMEOUT;
break;
}
}
}
}
if(i == 0)
{
length = buffer[0];
}
}
INTERRUPT_GlobalInterruptEnable();
SDA_OUTPUT();
delay_us(TX_DELAY); //forcing tTURNAROUND (To CryptoAuthentication)
return status;
}
如果MCU不是工作在16MHz下,pulse_length 的判断需要重新调整。
以上完成之后,需要实现几个HAL层的函数:
/**
* \brief Send byte(s) via SWI.
* \param[in] txdata pointer to bytes to send
* \param[in] txlength number of bytes to send
* \return ATCA_STATUS
*/
void hal_swi_send(uint8_t *txdata, int txlength)
{
//! Send Command Flag
swi_send_byte(SWI_FLAG_CMD);
//! Send the remaining bytes
swi_send_bytes(txlength, txdata);
}
/**
* \brief Receive byte(s) via SWI.
* \param[in] rxdata pointer to where bytes will be received
* \param[in] rxlength pointer to expected number of receive bytes to
* request
* \return ATCA_STATUS
*/
ATCA_STATUS hal_swi_receive(uint8_t *rxdata, uint16_t rxlength)
{
ATCA_STATUS status = ATCA_RX_TIMEOUT;
int retries = 8;
while(retries-- > 0 && status != ATCA_SUCCESS)
{
swi_send_byte(SWI_FLAG_TX);
status = swi_receive_bytes(rxdata, rxlength);
if(status == ATCA_SUCCESS)
return ATCA_SUCCESS;
delay_ms(2);
}
return status;
}
/**
* \brief Send Wake flag via SWI.
* \return ATCA_STATUS
*/
ATCA_STATUS hal_swi_wake()
{
ATCA_STATUS status = ATCA_WAKE_FAILED;
uint8_t response[4] = { 0x00, 0x00, 0x00, 0x00 };
uint8_t expected_response[4] = { 0x04, 0x11, 0x33, 0x43 };
swi_wake_token();
delay_ms(3);
status = hal_swi_receive(response, 4);
if(status == ATCA_SUCCESS)
{
//! Compare response with expected_response
if(memcmp(response, expected_response, 4) != 0)
status = ATCA_WAKE_FAILED;
}
return status;
}
/**
* \brief Send Idle flag via SWI.
* \return ATCA_STATUS
*/
void hal_swi_idle()
{
swi_send_byte(SWI_FLAG_IDLE);
}
/**
* \brief Send Sleep flag via SWI.
* \return ATCA_STATUS
*/
void hal_swi_sleep()
{
swi_send_byte(SWI_FLAG_SLEEP);
}
对于ATSHA204A常用的几个API 函数已经封装在atca_basic.c中,主要有:
void atCalcCrc() // 计算发送数据包中crc的值
uint8_t atCheckCrc(const uint8_t *response) // 检测收到的数据包中crc值中否正确
ATCA_STATUS atcab_read_serial_number(uint8_t* serial_number) //读序列号
ATCA_STATUS atcab_nonce(uint8_t *rand_out) //执行Nonce命令取得随机数
ATCA_STATUS atcab_mac(uint8_t *challenge, uint8_t *digest) //执行Mac指令取得信息摘要
因为身份验证时MCU也需要执行SHA256的算法,所以需要在代码中实现这个功能。具体参考sha2_routines.c
身份验证的过程可以参考以下代码,
ATCA_STATUS SHA204_MAC(void)
{
osc_init();
timer1_init();
if(atcab_random(challenge) == ATCA_SUCCESS)
{
challenge[30] = challenge[30] ^ challenge[31];
challenge[31] = challenge[31] ^ TMR1L;
if(atcab_mac(challenge, digest_sha204) == ATCA_SUCCESS)
{
memset(MAC, 0x00, 88);
memcpy(MAC, securit_key, 32);
memcpy(MAC+32, challenge, 32);
MAC[64] = 0x08;
MAC[79] = 0xEE;
MAC[84] = 0x01;
MAC[85] = 0x23;
sw_sha256(MAC, 88, digest_sw_sha);
if(memcmp(digest_sha204, digest_sw_sha, 32) == 0)
return ATCA_SUCCESS;
}
}
return ATCA_FUNC_FAIL;
}
其中用软件计算SHA256时的输入数据必须按下面的内容填充,要不然就得不到和SHA204A计算一样的结果。参考上面的代码中对MAC数组赋值的部分:
由于客户工程师软件开发能力有限,只会使用汇编语言,他们希望调用一个函数就可以实现了,所以这个函数中把MCU的初始化也加进去了。
主程序中直接调用SHA204_MAC函数即可完成一次验证过程,判断返回值为零时表示验证通过:
int main(void)
{
TRISB7 = 0;
LATB7 = 1;
while (1)
{
if(SHA204_MAC() == ATCA_SUCCESS)
LATB7 = !LATB7;
delay_ms(500);
}
}
为了提高别人破解的难度,在不影响正常功能的前提下,可以在代码中很多位置调用这个函数来验证。如果验证通不过,就不执行程序。
编译成功后,Flash代码接近6KB,Data接近1KB。
实际完成一次验证过程,对16MHz的PIC18,只需要130mS。
好的工具可以让调试事半功倍,这里请出神器Saleae Logic。配合最新版本的软件,可以分析Atmel SWI接口,能按位,字节和包的形式显示出采集的数据,并且可以显示数据包中的数据结构如指令和CheckSum等:
Tokens:
Bytes:
Packets:
附件工程有两个编译选项,可以选择编译成库或完整应用:
源码下载. 使用MPLAB XIDE V5.xx