Protocole SmartCard I2C. Échangez les commandes APDU via l'interface I2C

Présentation


Il y a quelque temps, j'ai participé au développement d'un appareil dans lequel il fallait implémenter la cryptographie russe. Puisqu'à l'avenir il était censé certifier cette décision, certaines exigences ont été avancées pour la mise en œuvre de la cryptographie. Et comme l'une des options pour simplifier la mise en œuvre de ces exigences, nous avons envisagé la possibilité d'intégrer un lecteur de carte à puce dans l'appareil ou d'installer une puce de carte à puce, dans laquelle de nombreux scénarios nécessaires de travail avec des informations clés ont déjà été mis en œuvre.

Malheureusement, une telle solution ne convenait pas pour une raison quelconque, bien que s'il avait été possible d'utiliser la cryptographie matérielle russe prête à l'emploi, cela aurait dû accélérer considérablement le développement et la certification ultérieure du produit final. Et les raisons de l'impossibilité d'utiliser des jetons USB ou des cartes à puce étaient très courantes: l'appareil aurait dû être assez compact (un petit module pour les appareils M2M ou IoT), fonctionner principalement en mode sans entretien et fonctionner dans une large plage de températures.

Dans cet article, je veux parler d'une solution possible à un tel cas en utilisant la puce A7001, qui est connectée au système via l'interface I2C.



Problèmes de mise en œuvre de la cryptographie dans le PAC


Je ne veux pas m'attarder sur les problèmes de certification cryptographique. Quiconque travaille avec cela le sait de toute façon, mais le reste ne semble pas en avoir besoin. Mais sur quelques points importants, cela vaut la peine d'être dit.

En théorie, il ne devrait pas y avoir de problèmes particuliers avec la cryptographie. Après tout, il suffit de prendre l'une des bibliothèques cryptographiques, par exemple, OpenSSL, ou toute autre parmi les nombreuses existantes.

Les problèmes commencent lorsque cette solution doit être certifiée. Et l'implémentation purement logicielle de la cryptographie dans le micrologiciel transforme l'appareil en un dispositif de protection des informations cryptographiques à part entière, qui nécessite une étude approfondie dans un laboratoire de test. Après tout, lors du développement d'une solution utilisant la cryptographie, vous devrez tôt ou tard penser à des choses comme un schéma de clés, le stockage de clés, la génération de nombres aléatoires et d'autres questions subtiles.

Il existe une méthode élégante pour implémenter des algorithmes cryptographiques russes certifiés pour certaines solutions, ce qui nous permet de simplifier légèrement le processus de création de terminaux et de réduire le temps de leur développement et de leur certification ultérieure. Il suffit d'intégrer une carte à puce ou une puce de carte à puce dans l'appareil, en l'utilisant comme une sorte de «racine de confiance», et ainsi de résoudre un nombre important de problèmes douloureux qui nécessitent de longues recherches et confirmations dans les laboratoires d'essais.



Microcontrôleur à carte à puce avec interface I2C


Pour écrire cet article, j'ai utilisé la puce A7001, qui se connecte à l'appareil final via le bus I2C, qui est disponible dans presque tous les appareils. La puce a été fournie par Aladdin RD , qui a déjà installé un firmware prenant en charge la cryptographie russe.

Le microcontrôleur A7001AG (microcontrôleur d'authentification sécurisée) est fabriqué par NXP. Selon la fiche technique de la puce, l' A7001AG est un microcontrôleur protégé contre les accès non autorisés basé sur l'architecture classique 80C51 avec un coprocesseur cryptographique.

En mode économie d'énergie, le microcontrôleur consomme 50 μA. Il prend en charge une tension d'alimentation comprise entre 1,62 V et 5,5 V et peut fonctionner à des températures de −25 ° C à + 85 ° C.

Pour interagir avec des appareils externes, l'interface esclave I2C est utilisée à une vitesse pouvant atteindre 100 kbit / s.

Le microcontrôleur est disponible dans plusieurs options de boîtier. Je me suis retrouvé au format HVQFN32. Il s'agit d'un boîtier en plastique mesurant 5x5x0,85 mm avec 32 contacts et un pas de 0,5 mm.

Aspect de l'affaire:



Son brochage:



Système hôte pour connecter la puce A7001


La carte ESP32 WiFi Kit 32 de Heltec a été prise comme disposition du système hôte avec l'interface I2C. Il coûte moins de 1000 roubles, possède toutes les interfaces filaires et sans fil nécessaires, il y a un connecteur pour connecter une batterie au lithium avec un circuit de charge, ainsi qu'un écran OLED de 0,96 pouce.



Un système presque parfait pour prototyper divers appareils IoT et M2M, avec lesquels j'ai longtemps voulu jouer.

La carte peut être programmée à la fois dans l'environnement de développement natif et dans l'IDE Arduino. Il existe de nombreux exemples d'utilisation. Pour plus de simplicité, j'ai opté pour l'IDE Arduino standard.

Schéma du circuit


Le schéma de circuit pour connecter la puce A7001 est illustré dans la figure. Il est légèrement différent de la fiche technique recommandée. Selon la description du constructeur, la borne 22 (signal de réinitialisation RST_N) devrait avoir un potentiel élevé, mais le circuit n'a pas démarré selon ce schéma. En raison du «coup scientifique», l'opérabilité a été obtenue en connectant une résistance de rappel R4 au conducteur de puissance négatif.

MISE À JOUR: Comme suggéré dans les commentaires, le schéma correspond à la fiche technique, tandis que la description de sortie m'a confondu
RST_N - Réinitialiser l'entrée, active LOW




Le circuit est assemblé sur une petite planche à pain. Les signaux d'alimentation et I2C sont connectés par quatre fils de connexion, et le module ESP32 lui-même est connecté à l'ordinateur via USB pour recevoir l'alimentation de l'ensemble du circuit et remplir le firmware.



Protocole de carte à puce I2C


Lorsque j'ai entendu parler pour la première fois de la connexion de microcontrôleurs de cartes à puce via le bus I2C, ils m'ont expliqué que la couche physique de l'interface de la carte à puce (GOST R ISO / IEC 7816-3-2013) avait été remplacée par I2C (SMBus), et tout le reste fonctionnait comme d'habitude. cartes à puce selon GOST R ISO / IEC 7816-4-2013 utilisant des commandes APDU.

Il s'est avéré que ce n'est pas tout à fait vrai, ou plutôt pas du tout. L'interaction avec le microcontrôleur à un niveau élevé se produit en utilisant des commandes APDU conventionnelles, mais il y avait aussi des «mais».

  1. L'interface I2C (SMBus) ru.wikipedia.org/wiki/I%C2%B2C est un bus avec adressage esclave, qui est fondamentalement différent de l'interface série UART, qui est conçu pour communiquer deux appareils sur une base point à point et sans utiliser l'adressage . Cela signifie que toutes les données transmises (commandes APDU) doivent être «compressées» au format de données du bus I2C.
  2. Le travail avec une carte à puce commence par sa réinitialisation, généralement en coupant l'alimentation, par exemple en retirant physiquement la carte du lecteur de carte. Après la réinitialisation, la carte à puce envoie d'abord le bloc de données ATR (Answer To Reset), qui contient les informations de configuration nécessaires pour configurer l'interaction avec la carte à puce.
    Et la puce sur le bus I2C ne fait pas exception, mais dans le cas où le microcontrôleur doit être soudé à la carte de circuit imprimé, il peut ne pas avoir de circuit d'alimentation du microcircuit ou de contrôle logiciel de la sortie de réinitialisation. Par conséquent, la réinitialisation de la puce est implémentée, y compris, au niveau des commandes du protocole I2C.

Ces problèmes et d'autres sont résolus par le biais du protocole de la carte à puce I2C, dont une description peut être trouvée sur le site Web de NXP à l'adresse www.nxp.com/docs/en/supporting-information/AN12207.pdf .

Partie logiciel


Une recherche dans la bibliothèque avec l'implémentation du protocole Smart Card I2C Protocol n'a donné aucun résultat. Par conséquent, j'ai dû comprendre les spécifications et faire la mise en œuvre des fonctions de base de ce qui était à portée de main.

Sketch sources pour 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); } 


Pour travailler avec le port I2C, j'ai utilisé la bibliothèque Wire standard. Je dois dire tout de suite que cette bibliothèque n'est pas adaptée à la mise en œuvre complète du protocole Smart Card I2C, car il ne permet pas de contrôler ACK et NACK lors de la transmission et de la lecture d'octets individuels, ce qui est nécessaire pour implémenter la réception correcte des données de longueur variable à partir d'une carte à puce.

Oui, et les exemples habituels du code Wire n'ont pas fonctionné la première fois, mais après avoir dansé avec un clavier à tambourin , plusieurs litres de café, googlé dans Yandex et Yandex dans google, une solution a été trouvée.

 Wire.write ( ); Wire.endTransmission (false); Wire.requestFrom (ADDR_A7001, 2, true); 

A en juger par la documentation de la bibliothèque, cette conception ne libère pas le bus I2C après avoir appelé endTransmission . Mais il s'est avéré que pour le module basé sur ESP32 que j'utilisais, le transfert de données ne se produit pas physiquement pendant l'appel à endTransmission (false) , comme écrit dans la documentation de la bibliothèque Wire, mais pendant l'appel à requestFrom (true) , alors que les données sont uniquement mises en file d'attente avant cette à transférer.

Compte tenu de ces limitations, j'ai dû faire des «béquilles», mais je voulais vraiment lancer la puce A7001 sans réécrire les bibliothèques standard. Pour cette raison, la gestion des erreurs de protocole n'a pas été implémentée et il n'a pas été possible non plus de recevoir des données de longueur variable (c'est-à-dire qu'il est toujours nécessaire de spécifier le nombre exact d'octets à lire).

De telles restrictions ne sont pas autorisées dans un système réel, mais ne sont pas essentielles pour démontrer l'utilisation des commandes APDU lorsque vous travaillez sur le bus I2C. Par conséquent, si une erreur se produit dans le protocole d'échange lors de l'échange de données via le port I2C, le commutateur à bascule d'alimentation est le nôtre.
En d'autres termes, si pendant la répétition de ces expériences tout fonctionnait et s'arrêtait soudainement avant de rechercher une erreur dans le code, éteignez et rallumez l'appareil. Avec un degré de probabilité élevé, cela peut résoudre le problème.

Exemples de code pour travailler avec la puce A7001


Dans les exemples, j'utilise plusieurs fonctions d'assistance:

vect_dump - sortie des données de vidage au format HEX sur le port de débogage;
sci2c_exchange - envoie un tableau de données via I2C et lit le nombre spécifié d'octets de réponse;
sci2c_status - lit l'état de réponse du microcircuit et, si nécessaire, affiche son état dans le port de débogage;
sci2c_apdu_send - envoie une commande APDU;
sci2c_apdu_recv - lit la réponse à la commande APDU.

Initialisation de la puce


Selon la description du protocole de la carte à puce I2C , avant de commencer à travailler avec la puce, trois commandes doivent être exécutées séquentiellement: redémarrage (réinitialisation à froid ou à chaud), lecture de l'ATR (lecture de la réponse à la réinitialisation) et configuration des paramètres d'échange (paramètre d'échange des appareils principaux). Et seulement après cela, la puce est prête à accepter les commandes APDU.

Réinitialisation logicielle


Tout est simple ici, nous envoyons une commande de redémarrage et attendons l'heure définie:

 sci2c_exchange ({0b00011111}, 2); delay(5); //      (tRSTG, time, ReSeT Guard) 

Lire la réponse pour réinitialiser


La lecture de l'ATR est un peu plus compliquée, car vous devez non seulement envoyer une commande, mais également lire les données de réponse. Selon la description du protocole, la taille maximale des données retournées CDBATS, MAX (Command Data Bytes, Answer To Reset, MAXimum) peut être de 29 octets.

 vect ATR = sci2c_exchange({0b101111}, 29+2); // 29  + 1  PCB + 1  —   vect_dump("ATR: ", ATR); 

Lire les données 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

Où 1E est la taille des données retournées (29 octets + 1 octet du PCB) et 00 est le PCB (Protocol Control Byte), qui devrait être égal à 0 et, apparemment, dans cet exemple, les données n'ont pas été lues correctement (il devrait y avoir un octet du PCB, et il y en a trois).

Les données suivantes sont encodées au format TLV:

B8h - Objet de données de bas niveau , taille 3 octets ( 11h 01h 05h );
B9h - Objet de données de liaison de protocole , de 2 octets ( 01h 01h );
BAh - Objet de données de couche supérieure , de 1 octet ( 01h );
BBh - Objet de données du système d'exploitation , 13 octets ( 41 37 30 30 31 43 47 20 32 34 32 52 31 ).

Décryptage de la configuration de lecture de la puce
Objet de données de bas niveau : 11h - versions majeures et mineures du protocole pris en charge.

Codes de détection d'erreur : 01h - prise en charge de la détection d'erreur et du contrôle d'intégrité des données transmises à l'aide du LRC (Longitudinal Redundancy Code).

Entier en attente de trame (FWI) : 05h - délai maximum entre deux commandes. La plage de valeurs peut aller de 10 ms à 5120 ms, la valeur par défaut est 5120 ms. La valeur est calculée par la formule T = 10 ms x 2 ^ FWI. Ce qui dans ce cas nous donne un retard de 320 ms (10ms x 2 ^ 5).

Objet de données de liaison de protocole - se compose de deux valeurs, 01h 01h , qui codent le protocole pris en charge et le protocole par défaut. Ces valeurs signifient la prise en charge du protocole APDU [GOST R ISO / IEC 7816-3-2013] et, comme vous pouvez le deviner, le même protocole est installé par défaut.

Objet de données de couche supérieure - le nombre 01h signifie la prise en charge du format APDU court et étendu.

L'objet de données du système d'exploitation est un identifiant d'une taille maximale de 15 octets, tel que défini dans la norme [GOST R ISO / IEC 7816-4-2013]. Dans notre cas, il s'agit de la chaîne " A7001CG 242R1 ".

Dispositif maître échange le paramètre


La dernière commande pour initialiser les paramètres d'échange:

 vect CDB = sci2c_exchange({0b11111111}, 2); sci2c_status("Status CDB: "); vect_dump("CDB: ", CDB, 1); 

Valeur de retour: CCh - (11001100b) selon la fiche technique, 4 et 5 bits doivent être une négation au niveau du bit des bits 2 et 3 (NNb code le CDBIMS au niveau du bit, MAX) et, selon la valeur codée, la puce prend en charge la taille de commande maximale possible de 252 octets CDBIMS , MAX (nombre d'octets de données de commande, maître à esclave, MAXimum).

Selon la description du protocole, après avoir exécuté ces trois commandes et dans cet ordre, le microcircuit est prêt à exécuter des commandes APDU ordinaires (bien qu'il semble fonctionner sans définir de paramètres d'échange, c'est-à-dire qu'il suffisait de faire une réinitialisation logicielle et de lire ATR).

Exécution des commandes APDU


Chaque cycle d'exécution des commandes APDU comprend les étapes suivantes:

  1. Envoyer APDU (commande de transmission de données maître à esclave).
  2. Attendez le temps de protection pour recevoir et traiter la commande.
  3. Attendez que le traitement de la commande lise l'état (commande Status).
  4. Lire les données de réponse (commande de transmission des données esclave à maître).

Cette logique est implémentée dans les fonctions sci2c_apdu_send et sci2c_apdu_recv , et il y a un point important ici: dans le format du protocole Smart Card I2C, il y a des compteurs des commandes APDU transmises. Ces compteurs doivent contrôler à la fois les dispositifs maître et esclave et ils sont conçus pour contrôler la séquence des données transmises, de sorte qu'en cas d'erreur de réception, il serait possible de transmettre ou de demander à nouveau des données APDU.

Des exemples de la mise en œuvre de ces fonctions peuvent être trouvés dans le code, et ci-dessous ne sont que des commandes APDU et des données de réponse.

Exemple de fiche technique:


C-APDU => 00 A4 04 04 04 54 65 73 74 00 - lire le fichier avec le nom "Test".
R-APDU <= 6A 86 - selon la fiche technique, la réponse devrait être 64 82 ( fichier ou application introuvable ), mais dans notre cas le firmware est téléchargé sur le microcircuit, et la réponse diffère de l'exemple décrit dans la documentation.

Cycle de vie de production des cartes de lecture


C-APDU => 80 CA 9F 7F 00
R-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 00

Lire Lire les informations de la carte



C-APDU => 80 CA 00 66 00
R-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

Lire Lire les informations clés


C-APDU => 80 CA 00 E0 00
R-APDU <= E0 12 C0 04 01 FF 80 10 C0 04 02 FF 80 10 C0 04 03 FF 80 10 90 00

En conclusion


Cette expérience de mise en œuvre de l'échange d'équipes APDU via l'interface I2C a été très intéressante. Je me suis même retrouvé à penser à plusieurs reprises que j'aimais résoudre divers problèmes dans le domaine des circuits, et aussi de la soudure ordinaire, depuis la dernière fois que j'ai dû prendre un fer à souder il y a plus de 5 ans.

J'espère que cet article sera utile et aidera à comprendre ceux qui s'intéressent à ce sujet. Écrivez si le matériel vous intéresse. Je vais essayer de répondre à toutes les questions de cet article, et si le sujet de l'utilisation du protocole Smart Card I2C est intéressant, je vais essayer de le divulguer plus en détail dans les publications suivantes.

Références:


Source: https://habr.com/ru/post/fr439574/


All Articles