引言
前一段时间,我参与了一种设备的开发,在该设备中必须实现俄罗斯密码学。 由于将来应该证明这一决定,因此对密码学的实施提出了某些要求。 作为简化这些要求的一种选择,我们考虑了将智能卡读取器集成到设备中或安装智能卡芯片的可能性,其中已经实现了处理关键信息的许多必要方案。
不幸的是,尽管有可能使用这种现成的解决方案,但由于某种原因,尽管可以使用现成的俄罗斯硬件加密技术,但这样可以大大加快最终产品的开发和后续认证的速度。 不能使用USB令牌或智能卡的原因非常普遍:该设备应该非常紧凑(用于M2M或IoT设备的小模块),主要在免维护模式下运行,并且可在较宽的温度范围内工作。
在本文中,我想谈谈使用A7001芯片针对这种情况的可能解决方案,该芯片通过I2C接口连接到系统。

在PAC中实施加密的问题
我不想详细讨论密码认证的问题。 与此相关的人无论如何都知道这一点,但是其他人似乎并不需要它。 但是仍然有一些重要的观点值得一提。
从理论上讲,密码学应该没有任何特殊的问题。 毕竟,采用其中一种加密库就足够了,例如,OpenSSL或许多其他现有的加密库。
当需要认证该解决方案时,问题就开始了。 固件中加密的纯软件实现将设备变成了成熟的加密信息保护设备,需要在测试实验室进行仔细研究。 毕竟,在使用加密技术开发解决方案时,迟早您将不得不考虑诸如密钥方案,存储密钥,生成随机数等细微问题。
对于某些解决方案,有一种优雅的方法可以实施经过认证的俄罗斯加密算法,这使我们能够稍微简化创建终端设备的过程,并减少其开发和后续认证的时间。 将智能卡或智能卡芯片嵌入设备中就足够了,以此作为“信心的根本”,从而解决了许多痛苦的问题,这些问题需要在测试实验室中进行长时间的研究和确认。

具有I2C接口的智能卡微控制器
在撰写本文时,我使用了A7001芯片,该芯片通过I2C总线连接到终端设备,该总线几乎可以在任何设备中使用。 该芯片由
Aladdin RD提供,该公司已经安装了支持俄罗斯密码的固件。
微控制器A7001AG(安全认证微控制器)由NXP制造。 根据芯片上的数据表,
A7001AG是基于经典80C51架构和密码协处理器的微控制器,
可防止未经授权的访问。
在省电模式下,微控制器消耗50μA。 它支持1.62V至5.5V的电源电压,可在−25°C至+ 85°C的温度范围内工作。
为了与外部设备进行交互,I2C从接口的使用速度高达100 kbit / s。
该微控制器有几种外壳可供选择。 我最终使用了HVQFN32格式。 这是一个塑料外壳,尺寸为5x5x0.85 mm,具有32个触点,间距为0.5 mm。
案件外观:

他的针脚:

用于连接A7001芯片的主机系统
Heltec的ESP32 WiFi Kit 32板被用作具有I2C接口的主机系统的布局。 它的价格不到1000卢布,具有所有必需的有线和无线接口,还有一个用于连接锂电池和充电电路的连接器以及0.96英寸的OLED显示屏。

我一直想使用的几乎完美的系统,用于为各种IoT和M2M设备制作原型。
该板可在本机开发环境和Arduino IDE中进行编程。 有许多使用它的示例。 为简单起见,我选择了标准的Arduino IDE。
电路图
图中显示了连接A7001芯片的电路图。
它与推荐的数据表略有不同。 根据制造商的描述,端子22(复位信号RST_N)应具有高电势,但电路并未按照此方案启动。 作为“科学戳”的结果,通过将上拉电阻器R4连接到负极电源线可实现可操作性。更新:如注释中所建议,该模式对应于数据表,而输出描述使我感到困惑RST_N-复位输入, 低电平有效

该电路组装在一个小面包板上。 电源和I2C信号通过四根连接线连接,ESP32模块本身通过USB连接到计算机,以为整个电路供电并填充固件。

智能卡I2C协议
当我第一次听说要通过I2C总线连接智能卡微控制器时,他们向我解释说,智能卡接口的物理层(GOST R ISO / IEC 7816-3-2013)已被I2C(SMBus)取代,其他所有操作都像往常一样符合GOST R ISO / IEC 7816-4-2013的智能卡,并使用APDU命令。
事实证明,这不是真的,或者根本不是。 使用常规的APDU命令确实会与微控制器进行高层交互,但是也存在一些“缺点”。
- I2C接口(SMBus) ru.wikipedia.org/wiki/I%C2%B2C是具有从属寻址的总线,它与串行UART接口有根本的不同,串行UART接口旨在点对点通信两个设备,而不使用寻址。 这意味着必须将所有传输的数据(APDU命令)“打包”为I2C总线数据格式。
- 智能卡的工作通常从关闭电源开始,例如从物理上将卡从读卡器中取出来进行复位。 重置后,智能卡首先发送ATR(重置响应)数据块,其中包含配置与智能卡交互所必需的配置信息。
I2C总线上的芯片也不例外,但是在微控制器应焊接到印刷电路板上的情况下,它可能没有微电路的电源电路或复位输出的软件控制。 因此,包括在I2C协议命令级别实现芯片的复位。
这些问题和其他问题在智能卡I2C协议框架内得到解决,有关描述,请访问NXP网站
www.nxp.com/docs/en/supporting-information/AN12207.pdf 。
软件部分
使用
智能卡I2C协议协议的库搜索未返回任何结果。 因此,我必须了解规范并实现现有功能的基本功能。
Arduino IDE的草图源#include <Wire.h> #include <vector> // I2C address on chip A7001 #define ADDR_A7001 static_cast<uint16_t>(0x48) using namespace std; typedef std::vector<uint8_t> vect; //-------------------------------------------------------------------------- // Output dump data by serial port void vect_dump(const char * prefix, const vect & v, const size_t start = 0, const size_t count = 0) { if(prefix) { Serial.print(prefix); } if(v.size() < start) { Serial.println("Empty"); return; } for(size_t i=0; i < (v.size()-start) && (count == 0 || i < count); i++) { uint8_t b = v[start + i]; // Format output HEX data if(i) Serial.print(" "); if(b < 0x0F) Serial.print("0"); Serial.print(b, HEX); } Serial.println(""); } //-------------------------------------------------------------------------- // Send array bytes by I2C to address A7001 and read response result_size bytes vect sci2c_exchange(const vect data, const uint8_t result_size) { Wire.beginTransmission(ADDR_A7001); Wire.write(data.data(), data.size()); Wire.endTransmission(false); Wire.requestFrom(ADDR_A7001, result_size, true); //delay(1); vect result(result_size, 0); if(result_size >= 2) { result[0] = Wire.read(); // Data size CDB result[1] = Wire.read(); // PCB for(size_t i=2; i<result.size()-2 && Wire.available(); i++) { result[i+2] = Wire.read(); } } return result; } //-------------------------------------------------------------------------- // Read Status Code uint8_t sci2c_status(const char * msg = nullptr) { vect v = sci2c_exchange({0b0111}, 2); uint8_t status = v[1] >> 4; if(msg) { Serial.print(msg); // Prefix switch(status) { case 0b0000: Serial.println("OK (Ready)"); break; case 0b0001: Serial.println("OK (Busy)"); break; case 0b1000: Serial.println("ERROR (Exception raised)"); break; case 0b1001: Serial.println("ERROR (Over clocking)"); break; case 0b1010: Serial.println("ERROR (Unexpected Sequence)"); break; case 0b1011: Serial.println("ERROR (Invalid Data Length)"); break; case 0b1100: Serial.println("ERROR (Unexpected Command)"); break; case 0b1101: Serial.println("ERROR (Invalid EDC)"); break; default: Serial.print("ERROR (Other Exception "); Serial.print(status, BIN); Serial.println("b)"); break; } } return status; } static uint8_t apdu_master_sequence_counter = 0; // Sequence Counter Master, Master to Slave //-------------------------------------------------------------------------- // Send APDU void sci2c_apdu_send(const vect apdu) { vect_dump("C-APDU => ", apdu); vect data(2, 0); // 0x00 - Master to Slave Data Transmission command + reserve to length data.insert(data.end(), std::begin(apdu), std::end(apdu)); data[0] |= (apdu_master_sequence_counter << 4); if(++apdu_master_sequence_counter > 0b111) { apdu_master_sequence_counter = 0; } data[1] = data.size() - 2; sci2c_exchange(data, 2); delay(10); sci2c_status(""); } //-------------------------------------------------------------------------- // Receive APDU vect sci2c_apdu_recv(uint8_t result_size) { Wire.beginTransmission(ADDR_A7001); Wire.write(0b0010); // 0010b - Slave to Master Data Transmission command Wire.endTransmission(false); Wire.requestFrom(ADDR_A7001, result_size, true); vect result(result_size, 0); for(size_t i=0; i<result.size() && Wire.available(); i++) { result[i] = Wire.read(); } vect_dump("R-APDU <= ", result); return result; } //-------------------------------------------------------------------------- void setup(){ Wire.begin(); Serial.begin(9600); while (!Serial); Serial.println(""); Serial.println("Smart Card I2C Protocol Arduino demo on A7001"); Serial.println(""); sci2c_exchange({0b00001111}, 2); //The bits b0 to b5 set to 001111b indicate the Wakeup command. sci2c_status("Status Wakeup: "); sci2c_exchange({0b00001111}, 2); //The bits b0 to b5 set to 001111b indicate the Wakeup command. sci2c_status("Status Wakeup: "); // Soft Reset sci2c_exchange({0b00011111}, 2); //The bits b0 to b5 set to 011111b indicate the Soft Reset command. delay(5); // Wait at least tRSTG (time, ReSeT Guard) sci2c_status("Status SoftReset: "); // Read ATR vect ATR = sci2c_exchange({0b101111}, 29+2); //The bits b0 to b5 set to 101111b indicate the Read Answer to Reset command. sci2c_status("Status ATR: "); vect_dump("ATR: ", ATR, 2); // Parameter Exchange // The bits b0 to b5 set to 111111b of the PCB send by the master device indicate the Parameter Exchange command. // The bits b6 and b7 of the PCB send by the master device code the CDBIsm,max(Command Data Bytes Integer, Slave to Master, MAXimum) vect CDB = sci2c_exchange({0b11111111}, 2); sci2c_status("Status CDB: "); vect_dump("CDB: ", CDB, 1); // Further examples of the exchange of APDU // Exchanges APDU from exmaple chapter sci2c_apdu_send({0x00, 0xA4, 0x04, 0x04, 0x04, 0x54, 0x65, 0x73, 0x74, 0x00}); sci2c_status("Status Test send: "); sci2c_apdu_recv(3+1); // R-APDU size + 1 byte PBC sci2c_status("Status Test recv: "); // Read Card Production Life Cycle sci2c_apdu_send({0x80, 0xCA, 0x9F, 0x7F, 0x00}); sci2c_status("Status card LC send: "); sci2c_apdu_recv(0x30+1); // R-APDU size + 1 byte PBC sci2c_status("Status card LC recv: "); // Read Card Info sci2c_apdu_send({0x80, 0xCA, 0x00, 0x66, 0x00}); sci2c_status("Status card info send: "); sci2c_apdu_recv(0x51+1); // R-APDU size + 1 byte PBC sci2c_status("Status card info recv: "); // Read Key Info sci2c_apdu_send({0x80, 0xCA, 0x00, 0xE0, 0x00}); sci2c_status("Status key send: "); sci2c_apdu_recv(0x17+1); // R-APDU size + 1 byte PBC sci2c_status("Status key recv: "); // Again exchanges APDU from exmaple chapter sci2c_apdu_send({0x00, 0xA4, 0x04, 0x04, 0x04, 0x54, 0x65, 0x73, 0x74, 0x00}); sci2c_status("Status Test send: "); sci2c_apdu_recv(3+1); // R-APDU size + 1 byte PBC sci2c_status("Status Test recv: "); Serial.println("Done!\n"); } //-------------------------------------------------------------------------- void loop() { delay(100); }
为了使用I2C端口,我使用了标准的Wire库。 我必须马上说,该库不适合完全实现智能卡I2C协议,因为 当发送和读取单个字节时,它不允许控制ACK和NACK,这是实现从智能卡正确接收可变长度数据所必需的。
是的,并且Wire代码的常规示例第一次无法使用,但是在使用
铃鼓键盘跳舞,几升咖啡,在Yandex中使用谷歌搜索和在Google中使用Yandex进行搜索之后,找到了解决方案。
Wire.write ( ); Wire.endTransmission (false); Wire.requestFrom (ADDR_A7001, 2, true);
从库文档判断,此设计在调用
endTransmission之后不会释放I2C总线。 但是事实证明,对于我使用的基于ESP32的模块,在调用
endTransmission(false)期间,实际上不会发生数据传输,就像在Wire库的文档中所写的那样,而是在调用
requestFrom(true)期间 ,而数据仅在此之前排队转移。
鉴于这样的限制,我不得不做一些“拐杖”,但是我真的很想在不重写标准库的情况下启动A7001芯片。 因此,未实现协议错误处理,也无法接收可变长度的数据(也就是说,您始终需要指定要读取的确切字节数)。
这种限制在实际系统中是不允许的,但对于在I2C总线上工作时演示APDU命令的使用并不是必不可少的。 因此,如果通过I2C端口交换数据时在交换协议中发生错误,那么我们将使用电源切换开关。
换句话说,如果在重复这些实验过程中一切正常并且突然停止,然后再寻找代码错误,请关闭电源然后再打开。 很有可能解决此问题。
使用A7001芯片的代码示例
在示例中,我使用了几个辅助函数:
vect_dump-将十六进制格式的转储数据输出到调试端口;
sci2c_exchange-通过I2C发送数据数组并读取指定数量的响应字节;
sci2c_status-读取微电路的响应状态,并在必要时在调试端口中显示其状态;
sci2c_apdu_send-发送APDU命令;
sci2c_apdu_recv-读取对APDU命令的响应。
微芯片初始化
根据对
智能卡I2C协议的描述,在开始使用芯片之前,应按顺序执行三个命令:重新引导(冷复位或软复位),读取ATR(读取对复位的应答)和设置交换参数(主设备交换参数)。 并且只有在此之后,芯片才准备接受APDU命令。
软重置
这里的一切都很简单,我们发送一个重新启动命令并等待设置的时间:
sci2c_exchange ({0b00011111}, 2); delay(5);
阅读答案以重置
读取ATR有点复杂,因为 您不仅需要发送命令,还需要读取响应数据。 根据协议描述,返回的数据CDBATS的最大大小MAX(命令数据字节,复位应答,MAXimum)可以为29个字节。
vect ATR = sci2c_exchange({0b101111}, 29+2);
读取ATR数据:
1E 00 00 00 B8 03 11 01 05 B9 02 01 01 BA 01 01 BB 0D 41 37 30 30 31 43 47 20 32 34 32 52 31其中1E是返回数据的大小(29字节+ 1字节的PCB),00是PCB(协议控制字节),应等于0,并且显然,在此示例中,数据未正确读取(PCB应该有一个字节,并且其中有三个)。
以下是以TLV格式编码的数据:
B8h- 低级数据对象 ,大小为3个字节(
11h 01h 05h );
B9h- 协议绑定数据对象 ,大小为2个字节(
01h 01h );
BAh- 高层数据对象 ,大小为1个字节(
01h );
BBh- 操作系统数据对象 ,13个字节(
41 37 30 30 31 43 47 20 32 34 32 52 31 )。
解密芯片的读取配置底层数据对象 :
11h-受支持协议的
主要版本和次要版本。
错误检测代码 :
01h-支持使用LRC(纵向冗余代码)进行错误检测和已传输数据的完整性控制。
帧等待整数(FWI) :
05h-两个命令之间的最大延迟。 值的范围可以从10毫秒到5120毫秒,默认值为5120毫秒。 该值由公式T = 10ms x 2 ^ FWI计算得出。 在这种情况下,我们的延迟为320毫秒(10毫秒x 2 ^ 5)。
协议绑定数据对象 -由两个值
01h 01h组成 ,它们对支持的协议和默认协议进行编码。 这些值表示支持APDU协议[GOST R ISO / IEC 7816-3-2013],并且,您可能会猜到,默认情况下会安装相同的协议。
高层数据对象 -数字
01h表示支持短和扩展APDU格式。
根据标准[GOST R ISO / IEC 7816-4-2013]的定义,
操作系统数据对象是最大15个字节的标识符。 在我们的例子中,这是字符串“
A7001CG 242R1 ”。
主设备交换参数
最后一个初始化交换设置的命令:
vect CDB = sci2c_exchange({0b11111111}, 2); sci2c_status("Status CDB: "); vect_dump("CDB: ", CDB, 1);
返回值:根据数据手册,CCh-(11001100b),第4位和第5位应为第2位和第3位的按位取反(NNb对按位取反的CDBIMS,MAX)进行编码,并且根据编码后的值,芯片支持最大可能的命令大小为252字节CDBIMS ,MAX(命令数据字节整数,从机到从机,MAXimum)值。
根据协议描述,在按顺序执行这三个命令之后,微电路已准备好执行常规的APDU命令(尽管它似乎无需设置交换参数即可工作,即足以进行软复位并读取ATR)。
执行APDU命令
每个执行APDU命令的周期包括以下步骤:- 发送APDU(主机到从机数据传输命令)。
- 等待保护时间以接收和处理命令。
- 等待命令处理以读取状态(状态命令)。
- 读取响应数据(从站到主数据传输命令)。
此逻辑是在
sci2c_apdu_send和
sci2c_apdu_recv函数中实现的 ,这里有一个重点:在智能卡I2C协议协议的格式中,有传输的APDU命令的计数器。 这些计数器必须同时控制主设备和从设备,并且它们被设计为控制发送数据的顺序,以便在出现接收错误的情况下,有可能再次发送或请求APDU数据。
这些功能的实现示例可在代码中找到,以下仅是APDU命令和响应数据。
数据表中的示例:
C-APDU =>
00 A4 04 04 04 54 65 73 74 00-读取名称为“ Test”的文件。
R-APDU <=
6A 86-根据数据表,答案应为
64 82 (
未找到文件或应用程序 ),但在我们这种情况下,固件已上传到微电路,答案与文档中描述的示例有所不同。
阅读卡生产生命周期
C-APDU =>
80 CA 9F 7F 00R-APDU <=
9F 7F 2A 47 90 51 67 47 91 12 10 38 00 53 56 00 40 39 93 73 50 50 12 35 63 00 00 00 00 00 13 2C 19 30 34 30 33 39 00 00 00 00 00 00 00 00 00 90 00读取读取卡信息
C-APDU =>
80 CA 00 66 00R-APDU <=
66 4C 73 4A 06 07 2A 86 48 86 FC 6B 01 60 0C 06 0A 2A 86 48 86 FC 6B 02 02 01 01 63 09 06 07 2A 86 48 86 FC 6B 03 64 0B 06 09 2A 86 48 86 FC 6B 04 02 55 65 0B 06 09 2B 85 10 86 48 64 02 01 03 66 0C 06 0A 2B 06 01 04 01 2A 02 6E 01 02 90 00读读关键信息
C-APDU =>
80 CA 00 E0 00R-APDU <=
E0 12 C0 04 01 FF 80 10 C0 04 02 FF 80 10 C0 04 03 FF 80 10 90 00总结
通过I2C接口实现APDU团队交换的经验非常有趣。 自从五年多前我不得不拿起烙铁以来,我什至发现自己想过几次,我喜欢解决电路领域以及普通焊接方面的各种问题。
希望本文对您有所帮助,并且有助于了解对此主题感兴趣的人。 如果您感兴趣,请写信。 我将尝试回答本文中的所有问题,如果使用智能卡I2C协议的主题很有趣,那么我将尝试在以下出版物中更详细地披露它。
参考文献: