Linux Embedded

Le blog des technologies libres et embarquées

TrustZone : sécuriser un client TLS avec OP-TEE

Introduction

En cybersécurité fabricants et développeurs s'efforcent de concevoir des systèmes sécurisés, tandis qu'en parallèle, des attaquants cherche constamment à découvrir des vulnérabilités. En effet, tout code ou schéma électronique peut exposer une faille. C'est pourquoi le terme même de "sécurité" est parfois remplacé par la notion de "confiance", en anglais trust. En ce sens, si un système semble suffisamment sécurisé, s'il semble invulnérable, alors il peut être considéré de confiance. Toutefois comment renforcer la sécurité d'un système supposé vulnérable ?

L'informatique a introduit la notion de niveaux de privilèges qui définissent différents niveaux de sécurité au sein d'un même système, soit différents niveaux de confiance. La société Arm a justement développé un nouveau niveau privilégié : la technologie TrustZone. Comme l'indique son nom elle permet l'implémentation d'un environnement de confiance supplémentaire.

C'est dans le contexte de mon stage de fin d'études chez Smile ECS que j'ai eu l'occasion d'approfondir ce sujet. Ainsi cet article vulgarise la technologie TrustZone et son utilisation au travers d'un Trusted Execution Environment (TEE), en particulier OP-TEE (Open Portable TEE). Pour démontrer les intérêts d'un TEE, l'article explique comment implémenter la sécurisation de la clé privée d'un client TLS, dans le cas de l'authentification mutuelle avec une serveur.

Déployer un TEE avec TrustZone

Dualité de mondes TrustZone

Développée dès 2002 la technologie Arm TrustZone est disponible sur l'ensemble des micro-processeurs Cortex-A d'architecture ARMv7 ou ARMv8. Et depuis plus récemment les micro-contrôleurs Cortex-M sous ARMv8-M supportent cette technologie. TrustZone fournit aux Cortex des instructions supplémentaires pour le contrôle d'un bit additionnel indiquant l'état courant du SoC (System On Chip). Ainsi TrustZone est une technologie on-chip qui dédouble le système en deux états d'exécutions : l'état sécurisé et l'état non-sécurisé. Les concepteur de SoC doivent alors tirer parti de ce bit supplémentaire pour cloisonner ces deux états.

Au niveau logiciel, un programme gère la bascule du système d'un état à l'autre. Ce programme est au niveau le plus privilégié : le niveau moniteur, le projet Trusted Firmware est généralement utilisé. Arm a défini une convention d'usage des deux états, appelés des mondes :

  • Le monde normal héberge l'environnement habituel (typiquement Linux et des applications). Or par sa diversité de fonctionnalités il est susceptible de présenter des vulnérabilités et n'est donc pas de confiance.
  • Le monde sécurisé à l'inverse est une Trust... Zone ! Les binaires contenus sont tous considérés de confiance : l'effort doit être fait pour les sécuriser. Ce monde est dédié aux opérations critiques.

La figure suivante schématise l'environnement logiciel sur ARMv8 :

Schéma de l'architecture ARMv8, Mondes TrustZone sur l'axe horizontal et niveaux de privilèges sur l'axe vertical.

 

Comme dans le monde normal, il reste à installer dans le monde sécurisé des applications et surtout un système d'exploitation. D'où l'intérêt d'un TEE !

TEE : un environnement d'exécution de confiance

Le Trusted Execution Environment ou TEE est un standard logiciel créé en 2010 par Global Platform. Le TEE est une définition d'architecture logicielle notamment décrite par des API (TEE Internal Core API et TEE Client API). Il propose un stockage sécurisé chiffré, des outils cryptographiques, et vise surtout à isoler du reste du système des applications de confiance. Ces Trusted Applications (TA) permettent l'exécution de codes critiques et ne sont exécutables qu'à travers une interface spécifique.

On retrouve dans le TEE la notion de monde sécurisé isolé de l'environnement normal, et en effet, c'est en s'appuyant sur TrustZone qu'un TEE est implémenté. Le TEE est en quelque sorte le système d'exploitation pour le monde sécurisé.

Certes mais à quoi sert un TEE ?

La figure suivante résume les différents éléments du système complet :

Schéma d'ensemble du système avec TEE détaillant les éléments à développer et ceux à sécuriser

 

La confiance dans le matériel est accordée dès son achat. Au niveau moniteur il suffit d'intégrer le Trusted Firmware, lui aussi considéré sécurisé. Du côté du monde normal il faut sélectionner la distribution Linux de son choix puis développer les applications souhaitées. Aucune effort supplémentaire n'est à faire sur leur sécurité ! Du côté monde sécurisé en revanche, des précautions doivent être prises pour développer les Trusted Applications. Enfin pour le TEE, il suffit d'en choisir un existant qui respecte les API.

Finalement le TEE permet des économies de moyens. D'abord en développement car la plupart des composants logiciels existent déjà. Mais surtout en sécurisation, car le TEE réduit la surface de logiciels critiques à sécuriser ou faire certifier.

⚠️ Nécessité du boot sécurisé

Le boot sécurisé est une fonctionnalité des SoC qui permet d'assurer l'authenticité et l'intégrité des images logicielles lancées au démarrage. L'excellent article de Mickaël Ramilison détaille ce sujet : Implémentation du Secure Boot sur iMX8.

Mais alors quel rapport avec les TEE ? Sans boot sécurisé n'importe qui peut compromettre l'image logicielle lancée, y compris les binaires du moniteur sécurisé, du TEE ou des Trusted Applications. Or si ces programmes sont altérés alors le monde sécurisé n'est plus de confiance. Par l'absurde, cette contradiction prouve la nécessité du boot sécurisé pour déployer un TEE.

OP-TEE le TEE open-source

Comme expliqué précédemment un TEE est d'abord un standard logiciel, et en pratique, il faut en choisir un déjà implémenté. OP-TEE est un TEE open-source reconnu et créé en 2014 par la société ST et l'association Linaro. Il implémente les fonctionnalités TEE Client API v1.0 et TEE Internal Core API v1.1.2 du standard TEE. (absence de la fonctionnalité Peripheral and Event API).

La figure suivante schématise ses composants principaux :

Schéma d'architecture d'OP-TEE

 

Officiellement OP-TEE propose pour certaines plateformes la génération complète d'une image système. Toutefois il reste possible d'installer manuellement les 3 composantes d'OP-TEE :

  • optee_os est le coeur du TEE et peut être compilé pour la plateforme cible. Le binaire tee-raw.bin produit est ensuite à intégrer à l'image système à démarrer, par exemple en configurant U-Boot pour l'utiliser. C'est aussi dans le dépôt d'optee_os qu'on retrouve le kit de développement des Trusted Applications.
  • optee_client est en charge d'appeler les Trusted Applications depuis le monde normal. Une fois compilée pour la plateforme cible, la librairie est à installer dans le Linux. Les headers présents dans le dépôt d'optee-client servent au développement des programmes appelant le TEE.
  • Enfin le client OP-TEE a besoin d'un driver Linux, ajouté au device tree :
	firmware {
        	optee {
                	compatible = "linaro,optee-tz";
                	method = "smc";
        	};
	};

Démonstration : mTLS, sécuriser la clé RSA du client dans le TEE

Le protocole TLS permet de sécuriser une communication numérique. Il garantit la confidentialité et l'intégrité de messages échangés entre un client et un serveur. Il assure également l’authenticité du serveur. Toutes ces garanties sont possibles grâce aux opérations cryptographiques effectuées. Sans rentrer dans les détails, ces dernières nécessitent une paire de clés asymétriques (clé privée et clé publique).

Le serveur possède une clé privée, qui comme son nom l'indique ne doit jamais être dévoilée. ⚠️ Les garanties de sécurité restent valables tant que ce secret n'est pas divulgué. En guise de clé publique le serveur émet un certificat, il est certifié par une Autorité de Certification (CA) ce qui empêche une attaque de l'homme du milieu (man in the middle). Le protocole TLS permet également une authentification mutuelle. Dans cette version (mTLS ou mutual TLS) le client possède aussi une paire clé privée et un certificat. Le serveur peut ainsi authentifier le client.

Cet article va maintenant détailler comment tirer parti d'un TEE afin d'y sécuriser la clé privée. Cette clé est manipulée par TLS pour une signature numérique, ainsi le TEE sera aussi en charge de cette opération. Cette démonstration s'inspire de celle de Krys Kwiatkowski, disponible sur son blog, qui sécurise la clé privé ECDSA d'un serveur. 

Ici, nous allons nous intéressé à sécuriser la clé privée RSA du client.

Schéma d'illustration de la démonstration mise en place

 

➡️ Cette démonstration est également disponible sur le dépôt GitHub associé à cet article : Securing BoringSSL client private key with OP-TEE.

Mise en place

Installer QEMU et OP-TEE

Pour cette démonstration OP-TEE sera utilisé sur une plateforme ARMv8 émulée par QEMU. Le projet OP-TEE propose tout le nécessaire pour générér cet environnement :

mkdir optee-qemuv8 && cd optee-qemuv8
repo init -u https://github.com/OP-TEE/manifest.git -m qemu_v8.xml
repo sync
cd build
make toolchains 
make QEMU_VIRTFS_AUTOMOUNT=y

Vous pouvez déjà démarrer le système :

mkdir qemu_hostfs # création d'un dossier partagé entre le PC et QEMU
cd optee-qemuv8/build
make QEMU_VIRTFS_ENABLE=y QEMU_VIRTFS_HOST_DIR=$PWD/../../qemu_hostfs/ run-only
(qemu) c

Deux terminaux s'ouvrent pour chacun des deux mondes TrustZone, connectez vous en tant que root dans le monde normal puis déplacez vous dans le dossier partagé : cd /mnt/host. Tous les exécutables ARMv8 produits par la suite dans cet article seront à copier dans qemu_hostfs/ pour être disponibles dans QEMU.

Préparation des clés

Comme expliqué précédemment nous cherchons ici à implémenter l'authentification mutuelle du TLS. Par conséquent le client comme le serveur doivent posséder chacun une paire de clés (clé privée et certificat public). Le certificat sert à attester de la bonne provenance de la clé publique présente dans celui-ci afin d'éviter une attaque man in the middle. Le certificat est certifié par une autorité de certification en qui avoir confiance. Cette CA possède également sa paire de clés.

Commençons par générer la paire de clés racines de la CA avec OpenSSL :

openssl req -x509 -days 7300 \ # Certificat x509 valable 20 ans
        -newkey rsa:2048 -keyout CA.key \ # RSA de 2048 bits
        -out CA.crt \ # Ce certificat est ici consid←r← de confiance
        -subj "/C=FR/ST=ARA/L=Grenoble/O=LinuxEmbedded/OU=SECS/CN=Certificate Authority" \
        -noenc

Il est ensuite possible de générer une paire de clés certifiée pour le serveur :

openssl req -new \
        -newkey rsa:2048 -keyout server.key \ # Cl← priv←e
        -out server.csr \
        -subj "/C=FR/ST=ARA/L=Grenoble/O=LinuxEmbedded/OU=SECS/CN=Server" \
        -noenc
openssl x509 -req -days 7300 -in server.csr \ # Certificat associ←
        -CA CA.crt \ # Ce certificat est certifi← par la CA
        -CAkey CA.key -sha256  \ # Hachage SHA256
        -CAcreateserial \
        -out server.crt
rm server.csr # Fichier interm←diaire

Vous pouvez de la même manière générer la paire de clés du client (➡️ commit associé).

Client-Serveur BoringSSL

Comme dans l'exemple de Krys, nous utiliserons la librairie BoringSSL. Cette librairie cryptographique développée par Google permet entre autres d'établir une communication TLS, mais surtout BoringSSL propose un moyen très simple pour surcharger l'opération de signature côté client !

A contrario la librairie MbedTLS, utilisée dans les systèmes embarqués, ne propose cette fonctionnalité que côté serveur et nécessite de passer par un driver PSA pour réaliser l'opération coté client. Il en est de même avec OpenSSL (dont BoringSSL est dérivé), il faut implémenter une engine OpenSSL pour obtenir cette fonctionnalité.

Téléchargez et compilez BoringSSL pour votre ordinateur :

git clone https://github.com/google/boringssl
cd boringssl
mkdir build && cd build
cmake .. && make

Une fois compilé sur le host, il est possible de lancer le serveur TLS à l'aide de l'outil bssl :

ifconfig # affichez votre IP
cd tool/ # Copiez vos clés à côté de l'exécutable bssl
./bssl server -accept 55555 -cert ./server.crt -key ./server.key

Le client sera lui exécuté dans l'environnement QEMU sous ARMv8, compilez maintenant BoringSSL pour cette architecture :

cd boringssl
mkdir build_arm && cd build_arm
# La toolchain armv8 a été compilée par OP-TEE QEMU :
CC=$PWD/optee-qemuv8/toolchains/aarch64/bin/aarch64-linux-gnu-gcc \
CXX=$PWD/optee-qemuv8/toolchains/aarch64/bin/aarch64-linux-gnu-g++ \ 
cmake .. && make

Cette fois nous compilerons notre propre client (➡️ commit) en reprenant le code source de l'outil bssl : 

# Import des sources bssl n←cessaires :
cp boringssl/tool/transport_common.* boringssl/tool/internal.h boringssl/tool/fd.cc client/
cd client/
# Compilation avec la toolchain AMRv8 :
CXX=$PWD/optee-qemuv8/toolchains/aarch64/bin/aarch64-linux-gnu-g++
BORINGSSL=$PWD/boringssl
$CXX -o client *.cc -Wall \
 -I$BORINGSSL/include/ $BORINGSSL/build_arm/ssl/libssl.a $BORINGSSL/build_arm/crypto/libcrypto.a # Lib BoringSSL

Une fois lancé en indiquant l'adresse IP du serveur et le port d'écoute, le client indique l'identité du serveur correctement vérifié avec le certificat racine CA.crt present au meme niveau que le binaire et chargé au démarrage :

Conection d'un client TLS simple depuis QEMU

Activer l'authentification mutuelle

Ici nous nous intéressons au cas de l'authentification mutuelle que nous allons activer au lancement du serveur :

./bssl server -accept 55555 -cert ./server.crt -key ./server.key -require-any-client-cert

Le serveur refusera tout client sans certificat, par conséquent, nous modifions le code du client pour charger sa paire de clés (➡️ commit) :

SSL_CTX_use_certificate_chain_file(ctx.get(), "client.crt");
SSL_CTX_use_PrivateKey_file(ctx.get(), "client.key", SSL_FILETYPE_PEM);

Et voilà ! Une fois recompilé et reconnecté, le client est correctement authentifié par le serveur :

Connection d'un client TLS au server avec authentification mutuelle

 

Sécurisation du client à l'aide d'OP-TEE

Il est maintenant temps de faire usage d'OP-TEE. La clé privée du client est actuellement en clair dans Linux. Un attaquant qui infecterait le système pourrait la voler ! Nous pourrions ajouter un mot de passe ou gérer l'accès au fichier, toutefois ce serait pas suffisant si l'attaquant exploite une vulnérabilité du kernel Linux. D'où l'intérêt du TEE : l'objectif ici est d'isoler la clé privée pour qu'elle ne soit jamais divulguée dans le monde normal. La clé privée est utilisée pour signer un message lors du protocole TLS, une Trusted Application dans le TEE devra donc se charger de ce calcul. Ainsi tant que les attaquants ne pourront pas infecter le TEE, la clé privée sera à l'abri.

Développer l'application de confiance

Nous allons préparer la Trusted Application qui sera en charge de la signature TLS. Il est conseillé de partir du modèle de référence hello world. Une TA doit implémenter différentes fonctions obligatoires, ensuite nous devons définir les commandes qui seront invocables depuis le monde normal (➡️ commit) :

TEE_Result TA_CreateEntryPoint(void) {/*...*/}
void TA_DestroyEntryPoint(void) {/*...*/}
TEE_Result TA_OpenSessionEntryPoint(uint32_t __unused param_types, TEE_Param __maybe_unused params[4], void __maybe_unused **sess_ctx) {/*...*/}
void TA_CloseSessionEntryPoint(void __maybe_unused *sess_ctx) {/*...*/}

static TEE_Result install_key(uint32_t param_types, TEE_Param params[4]) {/*...*/}
static TEE_Result has_key(uint32_t param_types, TEE_Param params[4]) {/*...*/}
static TEE_Result del_key(uint32_t param_types, TEE_Param params[4]) {/*...*/}
static TEE_Result sign_rsa(uint32_t param_types, TEE_Param params[4]) {/*...*/}

TEE_Result TA_InvokeCommandEntryPoint(void __maybe_unused *sess_ctx, uint32_t cmd_id, uint32_t param_types, TEE_Param params[4]) {
    switch (cmd_id) {
        case TA_INSTALL_KEYS: return install_key(param_types, params); // Stockage de la cl← dans le TEE
        case TA_HAS_KEYS: return has_key(param_types, params); // Tester la pr←sence d'une cl← 
        case TA_DEL_KEYS: return del_key(param_types, params); // Supprimer une cl←
        case TA_SIGN_RSA: return sign_rsa(param_types, params); // Op←ration de signature TLS
        default: return TEE_ERROR_BAD_PARAMETERS;
    }
}

Ici, aucune opération particulière n'est nécessaire à l'ouverture de la TA, le code pour les quatre commandes invocables est disponible dans ➡️ ce commit. Au minimum une paire de clés RSA est composée de trois valeurs numériques, c'est pourquoi nous définissons la structure rsa_pkey_t pour manipuler ce format de clés. Chaque clé dans le TEE est identifiée par le hach du nom du client (ici client).

Installation d'une nouvelle clé :

static TEE_ObjectHandle create_rsa_key(rsa_pkey_t *key) { 
    TEE_ObjectHandle obj = TEE_HANDLE_NULL;
	TEE_AllocateTransientObject(TEE_TYPE_RSA_KEYPAIR, key->n_s * 8, &obj);
    TEE_Attribute attrs[3]; // Cr←ation d'une cl← RSA au format OP-TEE
    TEE_InitRefAttribute(&attrs[0], TEE_ATTR_RSA_MODULUS, key->n, key->n_s);
    TEE_InitRefAttribute(&attrs[1], TEE_ATTR_RSA_PUBLIC_EXPONENT, key->e, key->e_s);
    TEE_InitRefAttribute(&attrs[2], TEE_ATTR_RSA_PRIVATE_EXPONENT, key->d, key->d_s);
	TEE_PopulateTransientObject(obj, attrs, 3);
    return obj;
}
static TEE_Result install_key(uint32_t param_types, TEE_Param params[4]) {
    TEE_ObjectHandle persistant_obj = TEE_HANDLE_NULL;
    rsa_pkey_t *key = (rsa_pkey_t *)params[1].memref.buffer; // Cl← priv←e ¢ installer pass←e en param│tre
    TEE_ObjectHandle transient_obj = create_rsa_key(key); // Cr←ation d'une cl← RSA OP-TEE
    uint8_t client_id[32]; // R←cup←ration de l'identifiant client (hach←) pass← en param│tre
    memcpy(client_id, params[0].memref.buffer, params[0].memref.size);  
    TEE_CreatePersistentObject(
        TEE_STORAGE_PRIVATE,           // Stockage de la cl← dans le TEE
        client_id, sizeof(client_id),  // Identifiant client
        TEE_DATA_FLAG_ACCESS_WRITE,    // ￉criture
        transient_obj,                 // Cl← ¢ stocker
        NULL, 0,
        &persistant_obj                // Handle de stockage
    );
    TEE_FreeTransientObject(transient_obj);
    TEE_CloseObject(persistant_obj);
    return TEE_SUCCESS;
}

Opération de signature :

static TEE_Result sign_rsa(uint32_t param_types, TEE_Param params[4]) {
    TEE_OperationHandle op = TEE_HANDLE_NULL;
    TEE_ObjectHandle key = TEE_HANDLE_NULL;
    uint8_t client_id[32]; // R←cup←ration de l'identifiant client (hach←) pass← en param│tre
    memcpy(client_id, params[0].memref.buffer, params[0].memref.size);  
	// Lecture de la cl← RSA stock←e :
    TEE_OpenPersistentObject(TEE_STORAGE_PRIVATE, client_id, 32, TEE_DATA_FLAG_ACCESS_READ, &key);
    TEE_AllocateOperation(&op, // Cr←ation d'une op←ration cryptographique
	TEE_ALG_RSASSA_PKCS1_PSS_MGF1_SHA256, // S←lection de l'algo de signature avec RSA et SHA256
    	TEE_MODE_SIGN,  // Mode signature
	MAX_RSA_KEY_SIZE * 8);
    TEE_SetOperationKey(op, key); // Association de la cl← ¢ l'op←ration
    TEE_AsymmetricSignDigest( // Signature
        op, NULL, 0, params[1].memref.buffer, params[1].memref.size, // Message ¢ signer pass← en param│tre
        params[2].memref.buffer, &params[2].memref.size); // Buffer de destination de la signature pass← en param│tre
    TEE_CloseObject(key);
    TEE_FreeOperation(op);
    return TEE_SUCCESS;
}

Une fois prête, il faut compiler la TA avec le kit de développement OP-TEE OS :

OPTEE_QEMU=$PWD/optee-qemuv8
cd ta/ && make \
 CROSS_COMPILE=$OPTEE_QEMU/toolchains/aarch64/bin/aarch64-linux-gnu- \ # Architecture cible
 BINARY=a3a8cd17-4156-41f5-8a66-fe2643a1c93e \ # Identifiant de la TA (fix← pr←alablement)
 -f $OPTEE_QEMU/optee_os/out/arm/export-ta_arm64/mk/ta_dev_kit.mk # Appel au kit de d←veloppement OP-TEE OS (make)
# (Fichiers n←cessaires : `sub.mk` et `user_ta_header_defines.h`)

Parmi les binaires produits, le fichier a3a8cd17-4156-41f5-8a66-fe2643a1c93e.ta est le programme de la TA. Ce binaire est chiffré par OP-TEE OS et peut ainsi être transmis tout en conservant la confidentialité du code, qui pour rappel, fait partie du monde sécurisé.

En effet OP-TEE n'utilise pas son espace mémoire sécurisé pour enregistrer le code des TA, les TA chiffrées sont sauvegardées dans le monde normal. C'est pourquoi en pratique un programme (le tee-supplicant) transmet la TA à OP-TEE en la récupérant à partir d'un emplacement donné.

Une fois compilée il faut donc placer le binaire de la TA au bon endroit :

cd /mnt/host
mv a3a8cd17-4156-41f5-8a66-fe2643a1c93e.ta /lib/optee_armtz

Déléguer la signature au TEE

Nous allons maintenant faire appel à cette application de confiance depuis le client. La ligne SSL_CTX_use_PrivateKey_file(...) est donc supprimée car la clé privée ne doit plus être dans le monde normal. Il faut maintenant surcharger le code BoringSSL de signature TLS pour faire appel à la TA dans le TEE, rien de plus simple avec la fonction suivante (➡️ commit) :

static const SSL_PRIVATE_KEY_METHOD prv_key_method = { .sign = tee_prv_key_sign, .decrypt = 0, .complete = 0};
/* ...*/
SSL_CTX_set_private_key_method(ctx.get(), &prv_key_method);

On peut ainsi développer son propre code pour faire appel au TEE :

enum ssl_private_key_result_t tee_prv_key_sign(
		SSL *ssl, uint8_t *out, size_t *out_len, size_t max_out, // message sign← ¢ renvoyer
		uint16_t signature_algorithm, const uint8_t *in, size_t in_len) { // message donn← ¢ signer

    uint8_t client_id_sha256[SHA256_DIGEST_LENGTH]; // Hachage de l'identifiant client pour identifier la cl← priv←e
    EVP_Digest("CLIENT_ID", strlen(CLIENT_ID), client_id_sha256, NULL, EVP_sha256(), NULL);

    uint8_t digest[SHA256_DIGEST_LENGTH]; // Hachage du message ¢ signer (c'est le protocole)
    EVP_Digest(in, in_len, digest, NULL, EVP_sha256(), NULL);

    TEEC_Context ctx; TEEC_Session sess;
	TEEC_Operation op; TEEC_UUID uuid = TA_UUID;
    TEEC_InitializeContext(NULL, &ctx); // Ouverture de la TA
    TEEC_OpenSession(&ctx, &sess, &uuid, TEEC_LOGIN_PUBLIC, NULL, NULL, NULL); // Session
    memset(&op, 0, sizeof(op)); // Pr←paration des 3 param│tres
    op.paramTypes = TEEC_PARAM_TYPES(TEEC_MEMREF_TEMP_INPUT, TEEC_MEMREF_TEMP_INPUT, TEEC_MEMREF_TEMP_INOUT, TEEC_NONE);
    op.params[0].tmpref.buffer = client_id_sha256;  // Identifiant client
    op.params[0].tmpref.size = SHA256_DIGEST_LENGTH;
    op.params[1].tmpref.buffer = digest;  // Message hach← ¢ signer
    op.params[1].tmpref.size = SHA256_DIGEST_LENGTH;
    op.params[2].tmpref.buffer = out;  // Buffer de sortie o r←cup←rer la signature
    *out_len = RSA_KEY_SIZE;
    op.params[2].tmpref.size = *out_len;
	// Appel de la TA (fonction de signature TA_SIGN_RSA)
    TEEC_Result res = TEEC_InvokeCommand(&sess, TA_SIGN_RSA, &op, NULL);
    if (res != TEEC_SUCCESS) {
        printf("TEEC_InvokeCommand failed with code 0x%x !", res);
        return ssl_private_key_failure;
    }
    TEEC_CloseSession(&sess);
    TEEC_FinalizeContext(&ctx);
    return ssl_private_key_success;
}

Pour compiler ce client modifié il faut maintenant inclure le client OP-TEE :

OPTEE_QEMU=$PWD/optee-qemuv8
$CXX -o client *.cc -Wall \
 -I$BORINGSSL/include/ $BORINGSSL/build_arm/ssl/libssl.a $BORINGSSL/build_arm/crypto/libcrypto.a \ # Lib BoringSSL
 -I$OPTEE_QEMU/optee_client/public $OPTEE_QEMU/out-br/target/usr/lib/libteec.so \ # Lib client OP-TEE
 -I../ta # Include de notre TA

Vous pouvez maintenant essayer ce nouveau client sécurisé et constater l'erreur suivante : TEEC_InvokeCommand failed with code 0xffff0008 !, c'est à dire TEEC_ERROR_ITEM_NOT_FOUND... C'est normal, la clé privée n'est pas installée !

Installer la clé privée dans le TEE

Le client sécurisé ne manipule plus lui même la clé privée, le TEE se charge de tout. Hors nous avons implémenté la commande TA_INSTALL_KEYS pour l'instant inutilisée. En effet il est toujours nécessaire d'installer la clé privée au sein du TEE. Dans cette démonstration nous allons développer un programme administrateur chargé d'installer cette clé privée.

Le code administrateur (➡️ commit) fait appel au TEE de la même manière que le client :

TEEC_InitializeContext(NULL, &ctx);
TEEC_OpenSession(&ctx, &sess, &uuid, TEEC_LOGIN_PUBLIC, NULL, NULL, NULL);
uint8_t client_id_sha256[SHA256_DIGEST_LENGTH]; // Hachage de l'identifiant client pour identifier la cl← priv←e
EVP_Digest(CLIENT_ID, strlen(CLIENT_ID), client_id_sha256, NULL, EVP_sha256(), NULL);

EVP_PKEY *pkey = PEM_read_PrivateKey(fopen("client.key", "r"), NULL, NULL, NULL); // Lecture du fichier cl←
RSA *rsa_pkey = EVP_PKEY_get0_RSA(pkey);
const BIGNUM *bnn, *bne, *bnd = NULL;
RSA_get0_key(rsa_pkey, &bnn, &bne, &bnd); // Extraction des valeurs RSA
rsa_pkey_t key; // Conversion du format BoringSSL vers notre structure rsa_pkey_t
BN_bn2bin(bnn, key.n); key.n_s = BN_num_bytes(bnn);
BN_bn2bin(bne, key.e); key.e_s = BN_num_bytes(bne);
BN_bn2bin(bnd, key.d); key.d_s = BN_num_bytes(bnd);

memset(&op, 0, sizeof(op)); // Pr←paration des 2 param│tres
op.paramTypes = TEEC_PARAM_TYPES(TEEC_MEMREF_TEMP_INPUT,TEEC_MEMREF_TEMP_INPUT, TEEC_NONE, TEEC_NONE);
op.params[0].tmpref.buffer = client_id_sha256; // Identifiant client
op.params[0].tmpref.size = SHA256_DIGEST_LENGTH;
op.params[1].tmpref.buffer = &key; // Cl← priv←e  ¢ installer
op.params[1].tmpref.size = sizeof(key);
// Appel de la TA (fonction d'installation TA_INSTALL_KEYS)
TEEC_InvokeCommand(&sess, TA_INSTALL_KEYS, &op, &err_origin);
TEEC_CloseSession(&sess);
TEEC_FinalizeContext(&ctx);

Le programme administrateur se compile de la même manière que le client en incluant la librairie du client OP-TEE. L'administrateur peut ensuite installer la clé privée dans le TEE :

cd /mnt/host
./admin put
rm admin client.key # L'admin "quitte" l'appareil

Voilà, la clé privée est uniquement dans le TEE, le client BoringSSL peut maintenant se reconnecter au serveur en utilisant la clé privée installée dans le mode sécurisé :

Connection du client TLS sécurisé par TEE

Conclusion

Cet article a expliqué ce qu'est TrustZone et comment cela permet le déploiement d'un TEE comme OP-TEE. Ici, nous avons isolé dans OP-TEE la clé privée d'un client TLS en utilisant une Trusted Application.

Cette démonstration pourrait trouver un cas d'usage réel par exemple sur une application bancaire. La banque (le serveur) a en effet besoin d'authentifier ses clients pour autoriser la consultation de leurs comptes. Sur un smartphone avec de nombreuses fonctionnalités le TEE peut permettre d'isoler facilement la clé privée. Elle est ainsi protégée des attaquants qui trouveraient une vulnérabilité critique du noyau Linux. Un TEE peut aussi présenter une vulnérablité, mais par conception ce scénario est toutefois considéré moins probable.

Un attaquant qui volerait le smartphone et l'infecterait pourrait toujours dialoguer avec la banque sous le nom du client. Dans ce scénario l'utilisateur peut heureusement faire bloquer son smartphone auprès de la banque. Protéger une clé privée n'est donc intéressant que contre un attaquant qui souhaite la voler à l'insu de l'utilisateur, car il pourrait ainsi usurper l'identité du client depuis un autre appareil. L'utilisateur pourrait mettre beaucoup de temps à s'en rendre compte.

Enfin il faut prendre conscience de la criticité de l'installation de la clé. Ici nous avons utilisé un programme administrateur qui, bien que temporairement, charge la clé privée depuis le monde normal. L'opération d'enrôlement du smartphone auprès de la banque est à concevoir avec précautions.

Cette démonstration peut servir d'inspiration pour développer un produit final avec une librairie TLS mieux reconnue (MbedTLS ou OpenSSL). Il serait également intéressant d'ajouter le support de plusieurs types d'algorithmes cryptographiques en plus du RSA-SHA256.

L'ensemble du code présenté dans cet article est disponible sur le Github de Smile ECS à l'adresse suivante : https://github.com/Openwide-Ingenierie/TLS-client-key-secure-TEE

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.