Introduction
Sécuriser l’environnement d'exécution dans un système embarqué est une nécessité apparue depuis les années 90 avec l’apparition des Secure Boot. Les solutions proposées étaient propriétaires et fragmentées entre les constructeurs. C’est alors dans les années 2010 que le projet open source Trusted Firmware [1] a été lancé visant à uniformiser les méthodes d'exécution d’environnements sécurisés. L'organisation Trusted Firmware se focalise sur les processeurs ARM Cortex et propose une implémentation exploitant la TrustZone de ces processeurs. La TrustZone est une barrière de sécurité physique dans le silicium du processeur qui permet l'isolation matérielle entre les environnements d'exécution et les périphériques [2].
Trusted Firmware-M
Avant de commencer l'intégration de TF-M avec Zephyr OS, je conseille de lire l'excellent article de Léo Briand sur Zephyr OS et MCUBoot, le bootloader utilisé par Zephyr OS. [3]
Pour cet article, je vais me concentrer sur le TF-M, l'implémentation de Trusted Firmware pour la famille de processeurs Cortex-M [4]. Le rôle de TF-M est de mettre en œuvre la communication logicielle entre ces domaines isolés au niveau matériel. TF-M offre des fonctionnalités de sécurité telles que des partitions sécurisées, des services cryptographiques, des niveaux d'isolation et un secure boot. Pour cela, TF-M définit deux principales zones d'exécution, le monde sécurisé SPE (Secure Processing Environment) et le monde non sécurisé (Non Secure Processing Environment).
C’est dans le monde sécurisé que TF-M, ses services et le secure bootloader vivent. Le monde non sécurisé est dédié à l'application, c’est ici que l’OS va être exécuté. Dans mon cas, ce sera Zephyr OS [5].
Une architecture typique utilisant TF-M ressemble à ceci :
L'API TF-M repose sur un ensemble d'APIs standardisées fournies par PSA (Platform Security Architecture). [6]
Pourquoi utiliser TF-M
Sécurité
À l’heure du CRA et de la prise de conscience que le système embarqué peut être un vecteur d’attaque, il est important de protéger son produit, et de garder en sécurité les secrets utilisés. Par exemple, un produit avec une stack réseau qui communique avec un serveur a besoin de certificats ou de clés de chiffrement pour pouvoir échanger de manière confidentielle et authentifiée. C’est ici que le TF-M entre en jeu, votre application exécute sa stack réseau et TF-M gère vos secrets, sans que vous ayez à vous soucier de comment les cacher. Grâce à la TrustZone, la stack réseau est physiquement séparée de vos secrets et une faille de sécurité côté applicatif ne peut pas impacter TF-M.
TrustZone TF-M contre Zephyr OS userspace [7].
Zephyr a une implémentation d’isolation d'exécution d’environnements divisant le monde en mode superviseur et mode user. Cela est réalisé grâce à un MPU (Memory Protection Unit). Mais Zephyr n’exploite pas la TrustZone car tout est exécuté dans le monde non sécurisé. C’est une excellente protection contre les bugs applicatifs mais une faille de sécurité dans le noyau Zephyr casse toutes les barrières logiques.
C’est pour cela que, pour des informations sensibles, TF-M est préférable.
Fonctionnalité
TF-M intègre un grand nombre de services, ces services peuvent être purement software ou utiliser des composants matériels en fonction des cartes utilisées.
- TFM_CRYPTO : Calcul cryptographique via PSA Crypto (chiffrement/déchiffrement, hash, signature/vérification, dérivation de clé, gestion de clés …). [8]
- TFM_INTERNAL_TRUSTED_STORAGE_SERVICE : Stockage interne sécurisé, isolé du monde non sécurisé, utilisé par TF-M/crypto pour stocker des clés. [9]
- TFM_PROTECTED_STORAGE_SERVICE : Stockage protégé externe pour le monde sécurisé (confidentialité, intégrité et authentification). [10]
- TFM_PLATFORM_SERVICE : Accès sécurisé aux composants hardware. [11]
- TFM_FIRMWARE_UPDATE_SERVICE : Mise à jour firmware en coopération avec MCUboot. [12]
- TFM_ATTESTATION_SERVICE : Attestation initiale du device, token signé contenant l'identité, l'état de boot, les mesures logicielles. [13]
Implémentation d'une application d'exemple
Pour cette application d'exemple, on se place dans le cas où MCUboot [14] est déjà intégré, la "chain of trust" est correcte et TF-M ainsi que l’application sont de confiance. Le fil d'exécution est donc donné au TF-M qui va ensuite exécuter notre application dans le monde non sécurisé.
Le but de l’application que l’on va implémenter est de demander à TF-M de générer une paire de clés privée/publique et de nous renvoyer la clé publique. Pour ce premier exemple, je me suis basé sur le fork Zephyr de NXP et j'ai travaillé sur la carte FRDM-RW612.
Un répertoire git est disponible contenant les applications développées dans cet article. TFM_integration_Zephyr [15]
Configuration de l'application
L’application se présente comme ceci :
/ns_app
├── CMakeLists.txt # Configuration de build
├── prj.conf # Configuration app/MCUBoot/TFM
└── src
└── main.c # Code sourcePour activer TF-M et MCUboot, on modifie le fichier de configuration de l’app prj.conf et on ajoute
CONFIG_BUILD_WITH_TFM=y
CONFIG_TFM_BL2=yBL2 pour "secondary bootloader" est le nom donné à MCUboot dans le projet TF-M. [16] TF-M utilise sa propre version de MCUboot afin de fournir une intégration cohérente.
Le code source de l’application reste minimal pour l’instant, avec un seul message.
#include <zephyr/kernel.h>
int main(void)
{
printk("Hello from non secure world\n");
return 0;
}
Build et exécution
Pour builder une application qui vit dans le monde non sécurisé, on ajoute le suffixe /ns à la commande de build et on flash. On utilise l'outil de gestion de projet de Zephyr "west" qui permet de mettre à jours nos paquets, build et flash notre application. [17]
west build -p always -b frdm_rw612/rw612/ns ns_app
west flash -d build
Lors du build, on a pu voir un message de warning disant que des dummy keys étaient utilisées :
CMake Warning
TFM_DUMMY_PROVISIONING is enabled:
The device will be provisioned using dummy keys and is NOT secure!
This is not suitable for production
Les dummy keys sont des clés directement dans le code source de BL2 et TF-M et donc connues de tout le monde. Fournir des clés en utilisant TF-M n’est pas aussi simple que lorsqu'on intègre MCUboot en standalone. Pour provisionner des clés, on ne va pas les configurer dans un fichier de config comme vu dans l’article de Léo Briand [3], mais utiliser la méthode de RAM provisioning. Cette méthode consiste à injecter une structure contenant nos secrets à une adresse spécifique, avant le boot de la carte. C’est BL2 et TF-M qui vont détecter que la carte est vierge et récupérer ces informations pour le provisioning. Grâce à cela, les clés ne sont jamais clairement affichées dans le code source de BL2 ou TF-M. Le provisionnement en RAM étant complexe et peu documenté, il pourrait donner lieu à un article complet dans le futur. [18]
En regardant les logs du boot on peut voir la chaîne d'exécution :
[INF] Starting bootloader
[WRN] This device was provisioned with dummy keys. This device is NOT SECURE
[INF] PSA Crypto init done, sig_type: EC-P256, using builtin keys
[INF] Image index: 1, Swap type: none
[INF] Starting bootloader
[INF] Beginning BL2 provisioning
[WRN] TFM_DUMMY_PROVISIONING is not suitable for production! This device is NOT SECURE
[INF] PSA Crypto init done, sig_type: EC-P256, using builtin keys
[INF] Image index: 1, Swap type: none
[INF] Image index: 0, Swap type: none
[INF] Bootloader chainload address offset: 0x20000
[INF] Image version: v0.0.0
[INF] Jumping to the first image slot
Booting TF-M v2.2.2+g357d3b50a
[INF] Beginning TF-M provisioning
[WRN] TFM_DUMMY_PROVISIONING is not suitable for production! This device is NOT SECURE
[WRN] This device was provisioned with dummy keys. This device is NOT SECURE
[Sec Thread] Secure image initializing!
Creating an empty ITS flash layout.
Creating an empty PS flash layout.
[INF][PS] Encryption alg: 0x5500100
[INF][Crypto] Provision entropy seed...
[INF][Crypto] Provision entropy seed... complete.
*** Booting Zephyr OS build v4.4.0-15149-g8557b5995e65 ***
Hello from non secure worldAu moment de l'exécution, le problème des dummy keys est rappelé. Comme le RAM provisioning n’a pas été effectué, on voit que BL2 et TF-M utilisent les dummy keys pour provisionner la carte initialement vierge. Durant la chaîne de boot, on voit que BL2 démarre, réalise son provisionnement puis démarre TF-M. Ensuite, TF-M réalise son provisionnement et donne le fil d'exécution à notre application.
Ajouter un service TF-M à notre application
Pour pouvoir utiliser un service de TF-M sur notre application, on doit ajouter la librairie spécifique au service. Dans notre cas, la librairie en question est PSA Crypto. Grâce à ce service, l’application va pouvoir demander à TF-M de générer des paires de clés et réaliser des opérations cryptographiques.
Dans le code source de l’application ajoutons :
#include <psa/crypto.h>Et vérifions que la connexion fonctionne :
int main(void)
{
printk("Hello from non secure world\n");
psa_status_t status;
status = psa_crypto_init();
if (status != PSA_SUCCESS) {
printk("psa_crypto_init failed: %d\n", status);
return 1;
}
printk("PSA Crypto init OK\n");
return 0;
}
Les logs de retour de l’application doivent ressembler à cela :
*** Booting Zephyr OS build v4.4.0-15149-g8557b5995e65 ***
Hello from non secure world
PSA Crypto init OKL’application arrive bien à communiquer avec TF-M.
Générer une paire de clés
Clé privée
Pour générer une paire de clés, on doit tout d'abord demander à TF-M de générer une clé privée. Pour cela, on lui donne un index de clé et TF-M va générer une clé privée liée à cet index. Le seul moyen de désigner cette clé est d’utiliser l’index donné au préalable. Il est impossible d’obtenir la clé privée. On observe bien le but de TF-M : on peut gérer des clés privées sans y avoir accès.
On définit notre index de clé 0x1000 comme une constante :
#define DEMO_ECC_KEY_ID ((psa_key_id_t)0x1000)
Pour générer une clé privée avec les attributs voulus, on utilise :
psa_status_t psa_generate_key(const psa_key_attributes_t * attributes,
psa_key_id_t * key);Pour cela, on peut créer une fonction generate_private_key() qui va construire la structure d'attributs et générer une clé privée liée à l’id donné. On peut aussi vérifier si l’id a déjà été utilisé pour ne pas générer de nouvelle clé.
#define ECC_KEY_BITS 256
psa_status_t generate_private_key(psa_key_id_t *key_id){
psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
psa_status_t status;
*key_id = PSA_KEY_ID_NULL;
psa_set_key_id(&attr, DEMO_ECC_KEY_ID);
psa_set_key_lifetime(&attr, PSA_KEY_LIFETIME_PERSISTENT);
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_DERIVE);
psa_set_key_algorithm(&attr, PSA_ALG_ECDH);
psa_set_key_type(&attr, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1));
psa_set_key_bits(&attr, ECC_KEY_BITS);
status = psa_generate_key(&attr, key_id);
psa_reset_key_attributes(&attr);
if (status == PSA_SUCCESS){
printk("created ECC private key, id=0x%x\n", (unsigned int)DEMO_ECC_KEY_ID);
return PSA_SUCCESS;
}
if (status == PSA_ERROR_ALREADY_EXISTS){
*key_id = DEMO_ECC_KEY_ID;
printk("using existing ECC private key, id=0x%x\n", (unsigned int)DEMO_ECC_KEY_ID);
return PSA_SUCCESS;
}
return status;
}Voici comment la structure se présente:
- Key_id : L’id de la clé définie en début de code
- Lifetime : Si la clé est persistante, dans notre cas la clé est conservée jusqu'à ce qu'on la supprime explicitement
- Usage Flag : Ce pour quoi on va utiliser la clé, dans notre cas dériver une clé publique
- Algorithm : Pour quel algorithme la clé peut être utilisée. Ici on choisit Elliptic Curve Diffie-Hellman qui sert à dériver un secret commun avec une autre entité
- Type: Permet de définir quel type de clé ECC que l’on veut
- Bits : permet de définir la taille
On peut appeler notre fonction et vérifier que l’id de notre clé est correct.
int main(void){
...
psa_key_id_t key_id;
status = generate_private_key(&key_id);
if (status != PSA_SUCCESS){
printk("generate_private_key failed: %d\n", status);
return 1;
}
printk("key_id=0x%x\n", (unsigned int)key_id);
return 0;
}
Les logs doivent ressembler à ceci :
*** Booting Zephyr OS build v4.4.0-15149-g8557b5995e65 ***
Hello from non secure world
PSA Crypto init OK
created ECC private key, id=0x1000
key_id=0x1000Sur une carte vierge, aucune clé n’a été générée avec notre id. Si on reset la carte, on peut voir que notre clé va être réutilisée.
*** Booting Zephyr OS build v4.4.0-15149-g8557b5995e65 ***
Hello from non secure world
PSA Crypto init OK
using existing ECC private key, id=0x1000
key_id=0x1000Maintenant, lorsque l’on veut utiliser cette clé, il suffit de fournir son id.
Dériver la clé publique
Pour dériver une clé publique on utilise :
psa_export_public_key(psa_key_id_t key,
uint8_t * data,
size_t data_size,
size_t * data_length);On donne d’abord l’id de la clé privée dont on veut dériver la clé publique. Puis on donne un tableau avec sa taille pour que TF-M y écrive la clé publique. Finalement, on fournit un pointeur pour que TF-M nous retourne la longueur de la clé dérivée.
L’appel à la fonction ressemble à cela, on peut aussi afficher la clé publique dans les logs :
int main(void){
...
uint8_t public_key[PSA_KEY_EXPORT_ECC_PUBLIC_KEY_MAX_SIZE(ECC_KEY_BITS)];
size_t public_key_len = 0;
status = psa_export_public_key(key_id, public_key, sizeof(public_key), &public_key_len);
if (status != PSA_SUCCESS){
printk("exporting public key failed: %d\n", status);
return 1;
}
printk("public key (%u bytes): ", (unsigned int)public_key_len);
for (size_t i = 0; i < public_key_len; i++) {
printk("%02x", public_key[i]);
}
printk("\n");
return 0;
}
Sans jamais avoir eu accès à la clé privée on peut récupérer la clé publique au niveau de notre application.
*** Booting Zephyr OS build v4.4.0-15149-g8557b5995e65 ***
Hello from non secure world
PSA Crypto init OK
using existing ECC private key, id=0x1000
key_id=0x1000
public key (65 bytes): 045d023a617f563c97bd3e52a55b1be506b4f1d41ef4a3aa3209024c8c5e15edcadeb655e7fc51b2814ea3171a8cdebc18c178d3a4b9df5202bc89149b85db0a78
Cette méthode peut être utilisée pour réaliser la procédure d'échange de clés de Diffie-Hellman [19] avec un serveur distant. Une fois cette clé publique dérivée, on peut envoyer notre clé sur l’Internet public à un serveur, réaliser la même procédure du côté serveur et récupérer sa clé publique. Une fois l'échange de clés publiques réalisé, on utilise l’algorithme ECDH HKDF (Elliptic Curve Diffie-Hellman Key Derivation Function) pour dériver un secret commun avec le serveur. On est finalement capable de communiquer avec un serveur distant de manière confidentielle sans jamais avoir eu accès aux secrets. Attention, rien n'empêche une attaque de type man-in-the-middle. Ici on se place dans le cas où l’on sait que le serveur est le bon et digne de confiance.
Partition sécurisée
Pour l’instant notre application communique directement avec TF-M, mais on peut vouloir limiter l’accès de notre application dans le monde non sécurisé aux services de TF-M. Pour cela, on peut créer une partition sécurisée et autoriser l’application à communiquer seulement avec notre service. [20]
Conceptuellement l’architecture ressemblerait à cela :
Grâce à ce partitionnement, il est possible de rajouter des niveaux d'isolation du côté du monde sécurisé grâce à des configurations de TF-M. Une partition sécurisée personnalisée peut donc être utilisée non seulement pour protéger les informations confidentielles de l'application non sécurisée, mais aussi pour organiser le monde sécurisé en composants plus petits et contrôlés.
Cette configuration se fait dans le fichier de configuration de l’application prj.conf :
CONFIG_TFM_ISOLATION_LEVEL=1?2?3L’isolation par défaut de niveau 1 sépare seulement le monde non sécurisé et le monde sécurisé.
Le niveau 2 ajoute un niveau d’isolation entre les services de TF-M, c'est-à-dire l'ARoT (Application Root of Trust services) et le PRoT (Platform Root of Trust services). Le PRoT comprend les composants les plus critiques de TF-M qui doivent à tout prix rester de confiance ; cela comprend les services cryptographiques, d'attestation ou les composants hardware. L'ARoT concerne des applications de plus haut niveau, celles qui vont s’occuper des mises à jour ou des stockages protégés externes pour l’application dans le monde non sécurisé.
Finalement, le niveau 3 isole chaque service les uns des autres.
Ces niveaux d'isolation vont impacter la manière dont les services peuvent interagir et communiquer entre eux. Ils ne pourront plus lire librement la RAM d’un autre service mais devront passer par le mécanisme PSA-FF-M avec messages, handles, buffers contrôlés, et changement de contexte/isolation. [21]
Voici une représentation des niveaux d'isolation :
Création d'une partition sécurisée
La structure d’une application avec un service sécurisé ressemble à ceci :
project/s_ns_app
├── CMakeLists.txt # Configuration de build
├── prj.conf # Configuration app/MCUBoot/TFM
├── src
│ └── main.c # Code source de l'application
├── tfm_post_config.cmake # Ajout de la partition sécurisée custom
└── secure_partition
├── CMakeLists.txt # Configuration de build du service sécurisé
├── manifest_list.yaml # Déclaration du service à TF-M
├── tfm_s_ns_service.yaml # Configuration du service sécurisé
└── tfm_s_ns_service.c # Code source du service sécurisé
tfm_s_ns_service.yaml permet de donner un nom à notre service, de définir quel est le modèle de communication, quelle est la fonction de point d'entrée quand un signal est détecté et qui a le droit de l’appeler. Un fichier de configuration peut ressembler à cela :
{
"psa_framework_version": 1.1,
"name": "TFM_SP_S_NS_SERVICE",
"type": "APPLICATION-ROT",
"priority": "NORMAL",
"model": "IPC",
"entry_point": "tfm_s_ns_service_req_mngr_init",
"stack_size": "0x800",
"services": [
{
"name": "TFM_S_NS_SERVICE",
"sid": "0xFFFFF101",
"non_secure_clients": true,
"connection_based": true,
"version": 1,
"version_policy": "STRICT"
}
]
}- On définit notre service comme application "root of trust", cela permet d’isoler notre service des services de TF-M de type Platform Root of Trust si on définit le niveau d’isolation au niveau 2.
- Le modèle IPC représente le moyen de communication avec notre service. Dans notre cas, cela permet de l’isoler des autres partitions et de communiquer via : Non-secure app -> appel PSA -> Secure Gateway -> SPM -> message IPC vers TFM_SP_S_NS_SERVICE.
- Ici notre fonction d'entrée est tfm_s_ns_service_req_mngr_init, c’est cette fonction qui va être exécutée quand TF-M détecte un signal de la part de notre application dans le monde non sécurisé.
- Définir non_secure_clients à true permet à notre application dans le monde non sécurisé d’appeler notre service.
- Connection_based force le client à ouvrir une connexion avec le SID de notre application avant de pouvoir réaliser des appels.
- Version_policy force le client à spécifier la version du service lors de la connexion.
C’est aussi dans services que l’on peut déclarer les dépendances dont notre service a besoin. On pourrait rajouter PSA CRYPTO afin d'imiter ce que l’on a réalisé dans la section précédente.
manifest_list.yaml permet de déclarer notre nouveau service à TF-M. Voici un exemple :
{
"description": "TF-M secure partition manifests",
"type": "manifest_list",
"version_major": 0,
"version_minor": 1,
"manifest_list": [
{
"description": "S/NS service partition",
"manifest": "tfm_s_ns_service.yaml",
"output_path": "secure_fw/partitions/s_ns_service",
"version_major": 0,
"version_minor": 1,
"linker_pattern": {
"library_list": [
"*tfm_*partition_s_ns_service*"
]
}
}
]
}Ici le plus important est de pointer vers le fichier de configuration tfm_s_ns_service.yaml que l’on a défini plus tôt.
Établir une connexion avec notre service sécurisé
Maintenant que notre service est paramétré, on peut établir une connexion avec notre app non sécurisée. Pour cela, on doit ajouter les librairies :
src/main.c
#include <psa/client.h>
#include <psa_manifest/sid.h>Client.h permet de faire des appels à des services de TF-M et sid.h permet d'appeler un service par son nom et sa version. Dans notre cas cela sera TFM_S_NS_SERVICE_SID et TFM_S_NS_SERVICE_VERSION. TFM_S_NS_SERVICE est le nom que l’on a donné à notre service dans tfm_s_ns_service.yaml
Avec ces librairies on peut établir une connexion avec notre service sécurisé.
#include <zephyr/kernel.h>
#include <psa/client.h>
#include <psa_manifest/sid.h>
int main(void) {
handle = psa_connect(TFM_S_NS_SERVICE_SID, TFM_S_NS_SERVICE_VERSION);
return 1;
}
Du côté service sécurisé, on implémente notre fonction de hook définie dans tfm_s_ns_service.yaml. De plus, on peut rajouter une fonction qui va réaliser différentes tâches en fonction du type de signal reçu par l’application.
secure_partition/tfm_s_ns_service.c
#include <stdio.h>
#include <stdint.h>
#include "psa/service.h"
#include "psa_manifest/tfm_s_ns_service.h"
// Dichotomie sur le type de signal
static void service_handle_signal(psa_signal_t signal)
{
psa_msg_t msg;
psa_status_t status;
status = psa_get(signal, &msg);
switch (msg.type) {
case PSA_IPC_CONNECT:
printf("secure : Connection requested from non secure world\r\n");
psa_reply(msg.handle, PSA_SUCCESS);
break;
case PSA_IPC_CALL:
status = service_echo(&msg);
psa_reply(msg.handle, PSA_SUCCESS);
break;
case PSA_IPC_DISCONNECT:
psa_reply(msg.handle, PSA_SUCCESS);
break;
default:
psa_panic();
}
}
// Fonction hook qui réagit à un signal de TFM
psa_status_t tfm_s_ns_service_req_mngr_init(void)
{
psa_signal_t signals;
while (1) {
signals = psa_wait(PSA_WAIT_ANY, PSA_BLOCK);
if (signals & TFM_S_NS_SERVICE_SIGNAL) {
service_handle_signal(TFM_S_NS_SERVICE_SIGNAL);
} else {
psa_panic();
}
}
return PSA_ERROR_SERVICE_FAILURE;
}On observe bien les 3 types de signaux que l’on peut recevoir : CONNECT, CALL et DISCONNECT. C’est dans le signal CALL que l’on va pouvoir aller récupérer des données du monde non sécurisé et renvoyer une réponse. Pour l'instant, la fonction service_echo(&msg) n'est pas encore implémentée.
Sur nos logs, on peut voir que le service sécurisé détecte bien la connexion.
*** Booting Zephyr OS build nxp-v4.1.0-48061-g8557b5995e65 ***
secure : Connection requested from non secure worldEnvoyer et recevoir des données du service sécurisé
Pour pouvoir échanger des données, on doit fournir un vecteur d’entrée et un vecteur de sortie à notre service. C’est avec cette structure de données que notre service sécurisé va pouvoir interagir et nous retourner des informations.
Un appel basique ressemble à ça :
src/main.c
#define IN_BUFFER_SIZE 64
#define OUT_BUFFER_SIZE 64
int main(void)
{
char request[] = "Ping";
char response[OUT_BUFFER_SIZE];
int in_vector_size = 1;
int out_vector_size = 1;
psa_invec in_vec[] = {
{
.base = request,
.len = sizeof(request)
},
};
psa_outvec out_vec[] = {
{
.base = response,
.len = sizeof(response)
},
};
psa_handle_t handle;
psa_status_t status;
handle = psa_connect(TFM_S_NS_SERVICE_SID, TFM_S_NS_SERVICE_VERSION);
printk("non secure : Sending %s to secure\n", request);
status = psa_call(handle, PSA_IPC_CALL, in_vec, in_vector_size, out_vec, out_vector_size);
psa_close(handle);
printk("non secure: Response from secure -> %s\n", response);
return 1;
}psa_call() prend en entrée la variable de connexion vue dans la section précédente, et le signal de type PSA_IPC_CALL. Ensuite on fournit notre vecteur d'entrée avec sa longueur. Vector size représente le nombre d'éléments dans notre vecteur d’entrée. Dans notre cas on fournit seulement une chaîne de caractères donc la taille de notre vecteur est 1. Cela permet de passer plusieurs éléments à la fois et de types différents. On fait de même pour le vecteur de sortie, notre service sécurisé pourra seulement renvoyer un élément.
Du côté du service sécurisé, on ajoute une fonction service_echo() qui va seulement remplir le vecteur de sortie avec une chaîne de caractères.
secure_partition/tfm_s_ns_service.c
#define IN_BUFFER_SIZE 64
#define OUT_BUFFER_SIZE 64
static psa_status_t service_echo(psa_msg_t *msg)
{
char call[IN_BUFFER_SIZE];
psa_read(msg->handle, 0, call, msg->in_size[0]);
printf("secure : Received from non secure -> %s\r\n", call);
char response[] = "Pong";
printf("secure : Sending %s to non secure\r\n", response);
psa_write(msg->handle, 0, response, sizeof(response));
return PSA_SUCCESS;
}On utilise psa_read() et psa_write() pour lire et écrire dans les vecteurs fournis par notre application dans le monde non sécurisé.
On observe dans les logs les messages Ping et Pong affichés dans les deux mondes.
*** Booting Zephyr OS build nxp-v4.1.0-48061-g8557b5995e65 ***
secure : Connection requested from non secure world
non secure : Sending Ping to secure
secure : Received from non secure -> Ping
secure : Sending Pong to non secure
non secure: Response from secure -> Pong
Grâce à TF-M qui supervise la communication entre les deux mondes, on a réussi à échanger des données entre le monde non sécurisé et le monde sécurisé à travers un service personnalisé.
La communication entre le monde sécurisé et le monde non sécurisé peut être représentée comme ceci :
Notre service n'écrit pas directement dans les vecteurs fournis par notre application. TF-M se charge de transmettre les données garantissant l’isolation des deux mondes.
Conclusion
On a pu voir dans cet article ce qu’est TF-M, et comment il permet de sécuriser votre appareil en exploitant la TrustZone. TF-M étant certifié PSA [22] selon les exigences ETSI EN 303 645 [23] et NIST 8259A [24], son intégration apporte un certain niveau de confiance à votre produit.
Nous avons également étudié deux exemples concrets d’utilisation de TF-M. Le premier montre comment utiliser un service natif de TF-M, à savoir PSA Crypto. Le second explique comment implémenter votre propre service sécurisé et établir une communication à l’aide du protocole IPC. Une simplification a été faite concernant le provisionnement des clés sur votre cible avec le RAM provisioning ; cette procédure mériterait un article à part entière.
Finalement, TF-M est là pour vous éviter d’implémenter vous-même toute votre sécurité. En moins d’une centaine de lignes de code, vos secrets sont protégés et gérés grâce à TF-M.
Références
[1] : Trusted Firmware
[2] : Arm TrustZone technology documentation
[3] : Article MCUBoot Léo Briand
[4] : Trusted Firmware-M documentation
[5] : Zephyr OS
[6] : PSA API avec TFM
[7] : Zephyr userspace
[8] : PSA Crypto
[9] : Internal Trusted Storage
[10] : Protected Storage
[11] : Platform
[12] : Firmware Update
[13] : Initial Attestation
[14] : MCUboot
[15] : Repo TFM application sample
[16] : Bootloader secondaire BL2
[17] : West
[18] : Trusted Firmware-M Ram provisioning
[19] : Diffie-Hellman key exchange
[20] : Trusted Firmware-M Custom secure partition
[21] : Trusted Firmware-M block diagram
[22] : PSA certification
[23] : ETSI EN 303 645
[24] : NIST 8259A