1. Introdução
Há algum tempo, participei do desenvolvimento de um dispositivo no qual era necessário implementar a criptografia russa. Como no futuro deveria certificar essa decisão, alguns requisitos foram apresentados para a implementação da criptografia. E como uma das opções para simplificar a implementação desses requisitos, consideramos a possibilidade de integrar um leitor de cartão inteligente ao dispositivo ou instalar um chip de cartão inteligente, no qual muitos cenários necessários para trabalhar com informações importantes já foram implementados.
Infelizmente, essa solução não se encaixou por algum motivo, embora se fosse possível usar a criptografia de hardware russa já preparada, isso aceleraria significativamente o desenvolvimento e a subsequente certificação do produto final. E as razões para a impossibilidade de usar tokens USB ou cartões inteligentes eram muito comuns: o dispositivo deveria ser bastante compacto (um pequeno módulo para dispositivos M2M ou IoT), operado principalmente no modo sem manutenção e funcionando em uma ampla faixa de temperatura.
Neste artigo, quero falar sobre uma possível solução para esse caso usando o chip A7001, que está conectado ao sistema através da interface I2C.

Problemas de implementação de criptografia no PAC
Não quero me debruçar sobre os problemas da certificação de criptografia. Quem trabalha com isso está ciente disso, mas o resto parece não precisar. Mas sobre alguns pontos importantes ainda vale a pena dizer.
Em teoria, não deve haver problemas especiais com criptografia. Afinal, é suficiente usar uma das bibliotecas criptográficas, por exemplo, OpenSSL ou qualquer outra dentre muitas existentes.
Os problemas começam quando é necessário que esta solução seja certificada. E a implementação puramente de software de criptografia no firmware transforma o dispositivo em um dispositivo de proteção de informações criptográficas completas, exigindo um estudo minucioso em um laboratório de testes. Afinal, ao desenvolver uma solução usando criptografia, mais cedo ou mais tarde você terá que pensar em coisas como um esquema de chaves, armazenamento de chaves, geração de números aleatórios e outros assuntos sutis.
Existe um método elegante para implementar algoritmos criptográficos russos certificados para algumas soluções, o que nos permite simplificar levemente o processo de criação de dispositivos finais e reduzir o tempo para seu desenvolvimento e subsequente certificação. É suficiente incorporar um cartão inteligente ou chip de cartão inteligente no dispositivo, usando-o como uma espécie de "raiz de confiança" e, assim, resolver um número significativo de questões dolorosas que exigem longa pesquisa e confirmação em laboratórios de teste.

Microcontrolador de cartão inteligente com interface I2C
Para escrever este artigo, usei o chip A7001, que se conecta ao dispositivo final via barramento I2C, disponível em praticamente qualquer dispositivo. O chip foi fornecido pela
Aladdin RD , que já possui firmware instalado que suporta criptografia russa.
O microcontrolador A7001AG (microcontrolador de autenticação segura) é fabricado pela NXP. De acordo com a folha de dados do chip, o
A7001AG é um microcontrolador protegido contra acesso não autorizado, com base na arquitetura clássica 80C51, com um coprocessador criptográfico.
No modo de economia de energia, o microcontrolador consome 50 μA. Ele suporta tensão de alimentação na faixa de 1,62V a 5,5V e pode ser operado em temperaturas de -25 ° C a + 85 ° C.
Para interagir com dispositivos externos, a interface escrava I2C é usada com uma velocidade de até 100 kbit / s.
O microcontrolador está disponível em várias opções de caixa. Acabei no formato HVQFN32. Trata-se de uma caixa de plástico medindo 5x5x0,85 mm com 32 contatos e um passo de 0,5 mm.
Aparência do processo:

Sua pinagem:

Sistema host para conectar o chip A7001
A placa ESP32 WiFi Kit 32 da Heltec foi tomada como o layout do sistema host com a interface I2C. Custa menos de 1000 rublos, possui todas as interfaces com e sem fio necessárias, há um conector para conectar uma bateria de lítio com um circuito de carregamento, além de um display OLED de 0,96 polegadas.

Um sistema quase perfeito para a criação de protótipos de vários dispositivos IoT e M2M, com os quais eu sempre quis brincar.
A placa pode ser programada no ambiente de desenvolvimento nativo e no IDE do Arduino. Existem muitos exemplos para trabalhar com ele. Para simplificar, optei pelo IDE padrão do Arduino.
Diagrama de circuito
O diagrama do circuito para conectar o chip A7001 é mostrado na figura.
É um pouco diferente da folha de dados recomendada. De acordo com a descrição do fabricante, o terminal 22 (sinal de redefinição RST_N) deve ter alto potencial, mas o circuito não foi iniciado de acordo com este esquema. Como resultado do “puxão científico”, a operabilidade foi alcançada conectando um resistor pull-up R4 ao condutor de potência negativo.ATUALIZAÇÃO: Conforme sugerido nos comentários, o esquema corresponde à folha de dados, enquanto a descrição da saída me confundiuRST_N - Redefinir entrada, ativo BAIXO

O circuito é montado em uma pequena placa de ensaio. Os sinais de energia e I2C são conectados por quatro fios de conexão, e o próprio módulo ESP32 é conectado ao computador via USB para receber energia de todo o circuito e preencher o firmware.

Protocolo I2C do cartão inteligente
Quando ouvi falar pela primeira vez sobre como conectar microcontroladores de cartão inteligente por meio do barramento I2C, eles me explicaram que a camada física da interface do cartão inteligente (GOST R ISO / IEC 7816-3-2013) foi substituída por I2C (SMBus) e tudo funcionou normalmente. cartões inteligentes de acordo com GOST R ISO / IEC 7816-4-2013 usando comandos APDU.
Descobriu-se que isso não é bem verdade, ou melhor, não existe. A interação com o microcontrolador em um nível alto ocorre usando os comandos APDU convencionais, mas também existem alguns "buts".
- A interface I2C (SMBus) ru.wikipedia.org/wiki/I%C2%B2C é um barramento com endereçamento escravo, fundamentalmente diferente da interface UART serial, projetada para comunicar dois dispositivos ponto a ponto e não usar endereçamento . Isso significa que todos os dados transmitidos (comandos APDU) devem ser "compactados" no formato de dados do barramento I2C.
- O trabalho com um cartão inteligente começa com sua redefinição, geralmente desligando a alimentação, por exemplo, removendo fisicamente o cartão do leitor de cartão. Após a redefinição, o cartão inteligente envia primeiro o bloco de dados ATR (Answer To Reset), que contém as informações de configuração necessárias para configurar a interação com o cartão inteligente.
E o chip no barramento I2C não é uma exceção, mas no caso em que o microcontrolador deve ser soldado à placa de circuito impresso, ele pode não ter um circuito de fonte de alimentação do microcircuito ou controle de software da saída de redefinição. Portanto, a redefinição do chip é implementada, incluindo, no nível dos comandos do protocolo I2C.
Esses e outros problemas são resolvidos pelo Protocolo Smart Card I2C, cuja descrição pode ser encontrada no site do NXP em
www.nxp.com/docs/en/supporting-information/AN12207.pdf .
Parte do software
Uma pesquisa na biblioteca com a implementação do
protocolo Smart Card I2C Protocol não retornou nenhum resultado. Portanto, eu tive que entender as especificações e fazer a implementação das funções básicas do que estava à mão.
Fontes de esboço para o IDE do Arduino#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); }
Para trabalhar com a porta I2C, usei a biblioteca Wire padrão. Devo dizer imediatamente que esta biblioteca não é adequada para a implementação completa do protocolo I2C do cartão inteligente, porque não permite controlar ACK e NACK ao transmitir e ler bytes individuais, o que é necessário para implementar a recepção correta de dados de comprimento variável de um cartão inteligente.
Sim, e os exemplos usuais do código do Wire não funcionaram pela primeira vez, mas depois de dançar com um teclado de
pandeiro , vários litros de café, pesquisando no Yandex e Yandex no Google, uma solução foi encontrada.
Wire.write ( ); Wire.endTransmission (false); Wire.requestFrom (ADDR_A7001, 2, true);
A julgar pela documentação da biblioteca, esse design não libera o barramento I2C após chamar
endTransmission . Porém, para o módulo baseado em ESP32 que eu usei, a transferência de dados não ocorre fisicamente durante a chamada para
endTransmission (false) , conforme escrito na documentação da biblioteca Wire, mas durante a chamada para
requestFrom (true) , enquanto os dados estão na fila somente antes disso. transferir.
Dadas essas limitações, eu tive que fazer algumas “muletas”, mas eu realmente queria lançar o chip A7001 sem reescrever as bibliotecas padrão. Por esse motivo, o tratamento de erros de protocolo não foi implementado e também não foi possível receber dados de tamanho variável (ou seja, você sempre precisa especificar o número exato de bytes a serem lidos).
Tais restrições não são permitidas em um sistema real, mas não são essenciais para demonstrar o uso de comandos APDU ao trabalhar no barramento I2C. Portanto, se ocorrer um erro no protocolo de troca ao trocar dados pela porta I2C, o comutador de alternância será nosso.
Em outras palavras, se durante a repetição dessas experiências tudo funcionou e parou repentinamente antes de procurar um erro no código, desligue e ligue novamente. Com um alto grau de probabilidade, isso pode resolver o problema.
Exemplos de código para trabalhar com o chip A7001
Nos exemplos, eu uso várias funções auxiliares:
vect_dump - envia dados de despejo no formato HEX para a porta de depuração;
sci2c_exchange - envia uma matriz de dados via I2C e lê o número especificado de bytes de resposta;
sci2c_status - lê o status da resposta do microcircuito e, se necessário, exibe seu status na porta de depuração;
sci2c_apdu_send - envia um comando APDU;
sci2c_apdu_recv - leia a resposta ao comando APDU.
Inicialização do microchip
De acordo com a descrição do
protocolo Smart Card I2C , antes de iniciar o trabalho com o chip, três comandos devem ser executados sequencialmente: Reinicialização (Reinicialização a frio ou suave), Leia ATR (Leia a resposta a redefinir) e Configure parâmetros de troca (Dispositivo mestre troca o parâmetro). E somente depois disso o chip está pronto para aceitar comandos APDU.
Redefinição suave
Tudo é simples aqui, enviamos um comando de reinicialização e aguardamos o tempo definido:
sci2c_exchange ({0b00011111}, 2); delay(5);
Leia a resposta para redefinir
A leitura do ATR é um pouco mais complicada, pois você precisa não apenas enviar um comando, mas também ler os dados da resposta. De acordo com a descrição do protocolo, o tamanho máximo dos dados retornados CDBATS, MAX (bytes de dados de comando, resposta para redefinição, MAXimum) pode ser de 29 bytes.
vect ATR = sci2c_exchange({0b101111}, 29+2);
Ler dados 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 31Onde 1E é o tamanho dos dados retornados (29 bytes + 1 byte do PCB) e 00 é o PCB (Protocolo Control Byte), que deve ser igual a 0 e, aparentemente, neste exemplo, os dados não foram lidos corretamente (deve haver um byte do PCB, e há três deles).
A seguir estão os dados codificados no formato TLV:
B8h -
Objeto de dados de baixo nível , tamanho 3 bytes (
11h 01h 05h );
B9h -
Objeto de dados de ligação de protocolo , 2 bytes de tamanho (
01h 01h );
BAh -
objeto de dados de camada superior , 1 byte (
01h ) de tamanho;
BBh -
objeto de dados do sistema operacional , 13 bytes (
41 37 30 30 31 43 47 20 32 34 32 52 31 ).
Descriptografia da configuração de leitura do chipObjeto de dados de baixo nível :
11h - versões
principais e secundárias do protocolo suportado.
Códigos de detecção de erros :
01h - suporte para detecção de erros e controle de integridade dos dados transmitidos usando LRC (código de redundância longitudinal).
Número inteiro de espera de quadro (FWI) :
05h - atraso máximo entre dois comandos. O intervalo de valores pode variar de 10 ms a 5120ms, o padrão é 5120ms. O valor é calculado pela fórmula T = 10ms x 2 ^ FWI. O que, neste caso, nos dá um atraso de 320 ms (10ms x 2 ^ 5).
Objeto de dados de ligação de protocolo - consiste em dois valores,
01h 01h , que codificam o protocolo suportado e o protocolo padrão. Esses valores significam suporte ao protocolo APDU [GOST R ISO / IEC 7816-3-2013] e, como você pode imaginar, o mesmo protocolo é instalado por padrão.
Objeto de dados de camada superior - o número
01h significa suporte para o formato APDU curto e estendido.
O objeto de dados do sistema operacional é um identificador de até 15 bytes de tamanho, conforme definido no padrão [GOST R ISO / IEC 7816-4-2013]. No nosso caso, esta é a sequência "
A7001CG 242R1 ".
Trocas de dispositivos principais Parâmetro
O último comando para inicializar as configurações de troca:
vect CDB = sci2c_exchange({0b11111111}, 2); sci2c_status("Status CDB: "); vect_dump("CDB: ", CDB, 1);
Valor de retorno: CCh - (11001100b), de acordo com a ficha técnica, os bits de 4 e 5 devem ser uma negação bit a bit dos bits 2 e 3 (NNb codifica o CDBIMS, MAX negado por bits) e, de acordo com o valor codificado, o chip suporta o tamanho máximo possível de comando de 252 bytes CDBIMS , Valor MAX (bytes de dados de comando inteiro, mestre para escravo, máximo).
De acordo com a descrição do protocolo, após executar esses três comandos e nessa ordem, o microcircuito está pronto para executar os comandos APDU habituais (embora pareça funcionar sem definir parâmetros de troca, ou seja, foi o suficiente para fazer um Soft Reset e ler o ATR).
Executando comandos APDU
Cada ciclo de execução de comandos APDU consiste nas seguintes etapas:- Enviar APDU (comando Master to Slave Data Transmission).
- Aguarde o tempo de proteção para receber e processar o comando.
- Aguarde o processamento do comando para ler o status (comando Status).
- Leia os dados de resposta (comando Escravo para transmissão de dados mestre).
Essa lógica é implementada nas
funções sci2c_apdu_send e
sci2c_apdu_recv , e há um ponto importante aqui: no formato do protocolo Smart Card I2C Protocol, existem contadores dos comandos APDU transmitidos. Esses contadores devem controlar os dispositivos mestre e escravo e são projetados para controlar a sequência de dados transmitidos, para que, em caso de erro de recepção, seja possível transmitir ou solicitar dados APDU novamente.
Exemplos da implementação dessas funções podem ser encontrados no código, e abaixo estão apenas os comandos APDU e os dados de resposta.
Exemplo da folha de dados:
C-APDU =>
00 A4 04 04 04 54 65 73 74 00 - leia o arquivo com o nome "Teste".
R-APDU <=
6A 86 - de acordo com a folha de dados, a resposta deve ser
64 82 (
arquivo ou aplicativo não encontrado ), mas, no nosso caso, o firmware é carregado no microcircuito e a resposta difere do exemplo descrito na documentação.
Ciclo de vida da produção do cartão de leitura
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 48 12 53 63 00 00 00 00 13 2C 19 30 34 30 33 39 00 00 00 00 00 00 00 00 90 00Leia as informações do cartão de leitura
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 63 06 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 00Leia as principais informações de leitura
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 00Em conclusão
Essa experiência de implementar a troca de equipes da APDU por meio da interface I2C foi muito interessante. Até me vi pensando várias vezes que gosto de resolver vários problemas do campo dos circuitos e também da soldagem comum, desde a última vez que tive que pegar um ferro de soldar há mais de 5 anos.
Espero que este artigo seja útil e ajude a entender os interessados neste tópico. Escreva se o material lhe interessar. Tentarei responder a todas as perguntas deste artigo e, se o tópico do uso do protocolo I2C do cartão inteligente for interessante, tentarei divulgá-lo com mais detalhes nas publicações a seguir.
Referências: