Linux Embedded

Le blog des technologies libres et embarquées

Mise à jour Over-The-Air de systèmes embarqués

Avec l'expansion de l'Internet des Objets, le nombre de systèmes intelligents connectés est en constante augmentation. Suivant leur application, ces objets connectés sont souvent très nombreux et hors de notre portée. Déployer des mises à jour manuellement via un accès physique peut donc devenir un véritable challenge en terme de temps et de coût.

Or, le déploiement de mise à jour est une partie essentielle dans la vie d'un système embarqué, que ce soit pour ajouter de nouvelles fonctionnalités, corriger des bugs ou des failles de sécurité. Un des exemples de ce besoin est l’attaque Mirai Botnet découverte en août 2016, qui consistait à s’introduire dans des systèmes Linux connectés en utilisant des noms d’utilisateurs et des mots de passe très répandus dans l’industrie. Le choix des identifiants d’accès doit donc être effectué de manière sûre et sécurisée.

La distribution de logiciels Over-The-Air (OTA) offre un moyen de mettre à jour tout un parc d’appareils à distance de manière sécurisée. Dans un tel écosystème, les objets à mettre à jour ne sont pas capables de refuser une mise à jour correcte ou de l’altérer. Voici un exemple de procédure générique pour mettre à jour un système embarqué en utilisant une méthode OTA (selon mender.io) :

[caption id="attachment_4664" align="aligncenter" width="674"] Source : www.mender.io[/caption]

D’après ce schéma, trois entités distinctes interviennent dans le processus de mise à jour. Nous allons trouver dans un premier temps un système de génération des différents logiciels. Ces machines disposent souvent d’une grande puissance de calcul, surtout lorsqu’il s’agit de générer des logiciels lourds tels que des images de systèmes d’exploitation. Ce système générera les différents artefacts permettant de mettre à jour les objets connectés. La compilation peut être lancée soit manuellement par un opérateur, soit automatiquement via un outil d’intégration continue. Ces artefacts sont ensuite téléchargés sur un serveur de gestion de mises à jour qui est régulièrement interrogé par les objets connectés. Une fois ces mises à jour en ligne, les objets peuvent les télécharger et les installer.

Rentrons maintenant plus en détail dans le processus d’installation. Une fois les mises à jour détectées, des tests sont effectués afin de savoir si elles sont compatibles avec les logiciels déjà présents sur l'appareil. Si ces tests sont validés, les mises à jour sont téléchargées, puis l’intégrité et l’origine des mises à jour sont vérifiées. Elles sont ensuite décompressées sur le disque de l’appareil (les paquets sont très souvent compressés afin de sauvegarder de la bande passante lors des transferts). Les nouveaux logiciels sont enfin installés et des étapes de pré/post-installation peuvent aussi être faites. Cette chaîne d’installation peut être résumée par le schéma suivant :

[caption id="attachment_4667" align="aligncenter" width="519"] Source : www.mender.io[/caption]

Nous noterons que d’après ce diagramme, tout système de mise à jour OTA doit être capable de détecter une mise à jour et la télécharger de manière sécurisée, de l’installer et vérifier si l’installation est réussie. S’il y a eu un problème lors de l’installation, le système doit pouvoir revenir à un état dans lequel une nouvelle tentative peut être effectuée. Il est avant tout nécessaire de laisser le système dans un état où il peut démarrer normalement.

Rappelons que dans la plupart des systèmes Linux, nous retrouverons les éléments suivants pouvant entrer en ligne de compte dans un contexte de mise à jour :

  • le bootloader
  • le kernel et les fichiers Device Tree (DT)
  • le système de fichiers racine (rootfs)
  • d’autres systèmes de fichiers montés après la racine
  • des logiciels spécifiques à une application

En règle générale, la mise à niveau d’un système Linux consiste à mettre à jour le kernel et le rootfs (et plus rarement le bootloader).

 

Stratégies de mise à jour

Mise à jour via un gestionnaire de paquets

Toutes les distributions Linux pour PC sont mises à jour avec un gestionnaire de paquets. Cependant, cette méthode comporte un certain nombre d’inconvénients quand il s’agit de systèmes embarqués. En effet, les logiciels des systèmes embarqués sont généralement prévus pour fonctionner sur une plateforme en particulier. Les images Linux s’exécutant sur ces systèmes forment donc un tout. On peut dire qu'elles constituent un ensemble indivisible. Or, en utilisant un gestionnaire de paquets, notre image ne possède plus ce caractère indivisible et devient plutôt une liste de paquets plus ou moins indépendants. Dès lors, il est plus compliqué de s'assurer qu'une librairie dans une certaine version n’engendre aucun conflit avec les autres paquets (et éventuels patchs) déjà installés. De plus, si le système redémarre au milieu de l’installation d’un paquet (coupure de courant par exemple), comment peut-on savoir que le paquet n’a pas été correctement installé pour pouvoir le réinstaller plus tard ? Tous ces inconvénients peuvent facilement rendre le système embarqué inopérationnel.

 

Mise à jour via un bootloader

La fonction principale d’un bootloader est de charger et démarrer un kernel. Cependant, la plupart sont dotés de fonctionnalités plus sophistiquées. Ils ont très souvent un interpréteur de lignes de commandes (shell) qui peut être utilisé via un périphérique d’entrée/sortie (liaison série par exemple). De plus, ils sont dotés d’un système de script qui permet à l’utilisateur de personnaliser leur comportement et ainsi d’implémenter un système de mise à jour compacte.

La conception d’un tel système au sein d’un bootloader peut cependant être une tâche difficile. Les bootloaders n’ont en effet qu’un support minimal des périphériques. Leur rôle principal étant de charger un kernel (qui possède en général un très bon support matériel), on consacre peu de temps au développement et à l'intégration de drivers. De plus, les drivers intégrés aux bootloaders sont peu voire pas du tout mis à jour, alors que leurs équivalents dans les kernels évoluent assez régulièrement. Certaines fonctionnalités peuvent donc présenter des failles de fonctionnement.

 

Mise à jour via une application

Une autre solution à envisager est de confier la responsabilité de la mise à jour à une application du système. A l'inverse d'un script exécuté par le bootloader, cette application bénéficierait de tous les services offerts par le système d'exploitation sous-jacent. Suivant le besoin du client et les ressources matérielles disponibles sur le système embarqué, plusieurs types de mise à jour peuvent être envisagés.

 

Mise à jour symétrique

Dans le mode de mise à jour symétrique, deux copies du rootfs, A et B, sont initialement installées. Dans un premier temps, le bootloader démarre le système contenu dans le rootfs A. Quand une mise à jour est disponible (et que tous les tests de vérification sont faits), la mise à jour est téléchargée et installée sur le rootfs B. Si l'installation se termine avec succès, l'application informe le bootloader de démarrer sur le rootfs B fraîchement mis à jour. Si par contre l'installation a échoué, le rootfs actif reste le rootfs A.

Ce système de mise à jour est extrêmement sûr : il assure qu’à tout moment le système est doté d'un logiciel fonctionnel, mais pas forcement à jour, avec toutefois la possibilité de le mettre à niveau plus tard. Des mécanismes de sécurité supplémentaires peuvent être mis en place : par exemple, si le système échoue à se mettre à jour pour la troisième fois consécutive, un opérateur est prévenu pour inspecter l’objet connecté.

Cependant, le défaut majeur de ce mode est l'espace mémoire de masse nécessaire, puisque deux rootfs complets sont constamment stockés.

 

Mise à jour asymétrique

Dans le mode de mise à jour asymétrique, nous disposons toujours de deux partitions. Cependant, seulement une des deux contient le rootfs. La seconde partition, généralement beaucoup plus petite, contient un système spécialisé dans la mise à jour du rootfs. Nous appellerons le système principal M (main) et le système de mise à jour U (update). Pour lancer une mise à jour du rootfs M, un redémarrage sur le système U est nécessaire. Ensuite, U télécharge la mise à jour et l’installe sur M. Une fois l’installation terminée, le système redémarre sur M.

Cette méthode possède quelques inconvénients : tout d’abord, contrairement à la mise à jour symétrique, si la mise à jour échoue, l’objet connecté n’est plus opérationnel, bien qu’il soit encore possible de le mettre à jour. Ensuite, la mise à jour requiert deux redémarrages, ce qui peut être gênant pour des applications critiques où le système embarqué ne peut rester inopérationnel qu’un court laps de temps.

D’un autre côté, la mise à jour asymétrique permet d'économiser une grande quantité de mémoire. Le kernel et le rootfs du système U sont généralement petits et peuvent être chargés intégralement en RAM via un initrd ou un initramfs. Il est également possible de compresser le kernel et le rootfs U ensemble, ce qui peut augmenter considérablement le ratio de compression et donc économiser encore plus d’espace.

 

Problématiques de sécurité

Dans beaucoup de domaines, la sécurité est un enjeu majeur, et la mise à jour de systèmes connectés ne fait pas exception. Cette application requiert d’ailleurs une attention toute particulière au niveau des moyens utilisés pour assurer la sécurité des mises à jour. Les objets connectés sont en effet très souvent connectés à un réseau, voire à Internet. Les paquets circulant sur ce réseau peuvent donc être interceptés, ce qui inclut les données échangées lors du processus de mise à jour. Il est donc impératif d’utiliser des outils de mise à jour et une infrastructure qui interdit à toute entité non-autorisée de venir lire et modifier les paquets de mise à jour. Ceci peut être effectué de deux manières : utiliser un canal de communication sécurisé pour transmettre les mises à jour (connexion HTTPS par exemple) et/ou signer les mises à jour pour que l’objet valide leur source. Il est bien entendu plus sûr de combiner ces deux méthodes afin de réduire les risques de piratage.

Nous nous intéresserons particulièrement au mécanisme de signature des mises à jour. Une signature numérique permet de garantir l’intégrité d’un document et d’en authentifier l’auteur. Le processus de signature est le suivant :

  • les données à signer sont passées dans une fonction de hachage. Il en ressort une empreinte binaire
  • cette empreinte est ensuite chiffrée en utilisant une clé privée conservée par le signataire, l’empreinte chiffrée est appelée signature
  • cette signature est adjointe aux données puis envoyée au destinataire
  • une fois les données reçues, le destinataire calcule leur empreinte en utilisant une fonction de hachage
  • parallèlement, il déchiffre la signature avec la clé publique que le signataire lui a confiée, pour obtenir l’empreinte envoyée par l’émetteur
  • si les deux empreintes sont les mêmes, l’intégrité et l’origine des données sont confirmées

Le process peut être illustré par la figure suivante :

 

Plusieurs algorithmes peuvent être utilisés dans ce type de mécanisme, selon la fonction à réaliser et les performances souhaitées. Ainsi, pour le hachage des données nous pourrons utiliser SHA-2 ou SHA-3 (Secure Hash Algorithm), il est déconseillé d’utiliser SHA-1 étant donné que cet algorithme présente des failles de sécurité connues. Pour le chiffrement, il est possible d’utiliser RSA avec des clés d’une longueur de 2048 bits ou plus. Pour ce qui est de la confiance dans les clés échangées, il existe deux types de méthodes : des méthodes décentralisées comme PGP (Pretty Good Privacy) basées sur la transitivité de la confiance (les amis de mes amis sont mes amis) où les clés sont échangées de proche en proche, et les méthodes centralisées dites PKI (Public Key Infrastructure) dans lesquelles les certificats seront attribués par des autorités spécifiques, capables de signer et révoquer les certificats.

 

Comparatif de trois solutions de mise à jour

Un certain nombre de solutions de mise à jour OTA ont été conçues ces dernières années. Nous nous intéresserons dans cette partie à trois d’entre elles, qui semblent faire partie des solutions open-source les plus populaires.

 

Mender

Comme l’indique l’équipe de Mender sur son site web, le but de ce projet est de participer à la sécurisation des objets connectés en leur fournissant un processus de mise à jour simple et robuste. Dans son utilisation, Mender est en effet assez simple. L’intégrateur aura seulement besoin d’ajouter sa couche BSP (Board Support Package) spécifique au matériel utilisé, ainsi que la configuration du client de mise à jour. La compilation nécessite cependant l’utilisation de Yocto, ce qui peut perturber les non-initiés (la compilation reste possible sans Yocto mais semble plus complexe).

Mender est livré avec un mécanisme de rollback intégré (retour en arrière lorsqu’une installation échoue) ce qui est un véritable avantage pour ce système. Généralement, cette fonctionnalité requiert un moyen de communiquer avec le bootloader, et à l’heure actuelle Mender ne supporte que le bootloader U-boot.

Un serveur est également fourni par Mender. Il implémente tout le mécanisme de transmission des mises à jour. En utilisant ce serveur, la sécurité est aussi renforcée puisque la communication entre le client et le serveur ne peut se faire qu’en utilisant le protocole HTTPS. Il est également possible de recevoir les mises à jour depuis d’autres sources telles qu’un système de fichier local (clé USB par exemple) ou depuis une URL. Mender insistant grandement sur la sûreté de leur mise à jour, il ne propose qu’un mécanisme de mise à jour symétrique, ce qui permet d’offrir la fonctionnalité de rollback.

D’autre part, la structure de Mender impose un minimum de 4 partitions sur le disque du client : une partition de démarrage, 2 partitions rootfs (mise à jour symétrique), et une partition de données pour les fichiers de configuration de Mender notamment. Il est aussi nécessaire de réserver un espace mémoire supplémentaire pour stocker les rootfs reçus lors des mises à jour.

 

Swupdate

Swupdate est également un projet offrant à des appareils embarqués exécutant un système Linux la capacité de se mettre à jour. A l’inverse de Mender, Swupdate est considéré comme un framework, apportant tous les outils nécessaires pour effectuer des mises à jour, dans lequel d’autres protocoles d’installation peuvent être ajoutés afin de personnaliser le processus et le faire correspondre plus facilement à nos besoins. Il peut être utilisé avec Yocto, Buildroot, ou bien compilé et installé sur un rootfs manuellement.

Comme pour Mender, les mises à jour peuvent être récupérées depuis un serveur distant (avec une connexion sécurisée type HTTP(s)) ou depuis un média connecté localement. Swupdate est capable d’interagir avec un logiciel d’interface permettant de communiquer avec un serveur Hawkbit (serveur de mise à jour écrit en Java faisant partie du projet Eclipse). Swupdate embarque également un serveur Web (optionnel) sur lequel il est possible de se connecter pour transférer les mises à jour et les installer. Les deux stratégies de mises à jour asymétrique et symétrique présentées plus haut sont toutes les deux nativement supportées. Il est aussi possible de ne mettre à jour qu’un seul fichier parmi tout le système de fichier, là où Mender nous oblige à écraser tout un rootfs. De plus, Swupdate supporte les bootloaders GRUB et U-boot.

Comme mentionné plus haut, il est possible de personnaliser l’installation des mises à jour. Ceci est effectué de deux manières : avec des scripts de pré/post-installation, ainsi qu’avec des scripts plus spécifiques, appelés ‘handlers’ permettant d’ajouter un installateur pour un type d’image ou de matériel non supporté nativement (par exemple mettre à jour un microcontrôleur via une liaison série).

Ainsi, Swupdate offre une certaine flexibilité mais apporte en contrepartie quelques difficultés d’intégration. L’un des points notables est la nécessité de concevoir soi-même le mécanisme de retour en arrière (rollback) qui n’est pas intégré par défaut. Swupdate propose sur leur dépôt Github un ensemble de recettes Yocto pour Raspberry Pi 3 et BeagleBone incluant un exemple de script shell assurant la fonctionnalité de rollback.

 

Rauc

Rauc, au même titre que Swupdate, se veut être un cadre de travail sur lequel on vient greffer les éléments que l’on désire. Dans cette optique, Rauc va assez loin puisqu’il propose une totale personnalisation du logiciel et ce via plusieurs aspects.

Premièrement, Rauc possède un module d’interface pour les bootloaders les plus courants (U-boot, Grub, Barebox) que l’on peut facilement remplacer par une interface personnalisée. Il est ainsi possible de supporter n’importe quel bootloader du moment que le système d’exploitation est capable de discuter avec lui. Rauc supporte les mises à jour asymétrique et symétrique, et le partitionnement du disque est totalement libre (choix du nombre de rootfs, partitions data indépendantes ou rattachées à un rootfs, etc).

Deuxièmement, il est possible de modifier le comportement de Rauc au niveau de la mise à jour en ajoutant des scripts de pré/post-installation comme Swupdate, mais aussi des scripts d’installation, ce qui a comme conséquence de totalement modifier la procédure de mise à jour. Ces scripts sont appelés ‘handlers’. Ils sont stockés sur le client et exécutés pour toutes les mises à jour reçues.

Enfin, le dernier niveau de personnalisation se fait au niveau des paquets des mises à jour. Rauc offre la possibilité d’y insérer des scripts appelés cette fois ‘hooks’ (crochets) qui s’appliquent donc pour une mise à jour spécifique. Il est aussi possible de concevoir des ‘hooks’ s’exécutant pour un ‘slot’ en particulier (un ‘slot’ étant un emplacement mémoire susceptible d’être mis à jour).

Les sources des mises à jour sont multiples, allant du média local au serveur distribuant les paquets de mises à jour. Rauc dispose d’ailleurs d’une interface D-Bus, qu’il est possible d’utiliser depuis des clients chargés de communiquer avec un serveur spécifique. Rauc met à disposition un client Hawkbit utilisant cette interface D-Bus. Cependant, il est écrit en python et nécessite d’embarquer l’interpréteur sur le système de fichiers, ce qui est assez coûteux en terme d'espace mémoire. Il est cependant possible de réécrire ce client aisément en C/C++. Si cette interface-là n’est pas utilisée, Rauc est le système de mise à jour le plus léger et le plus flexible parmi les trois étudiés. Son intégration peut être effectuée avec Yocto, PTXDist ou bien manuellement. Avec Yocto, l’intégrateur aura à ajouter sa couche BSP bien sûr, mais aura aussi à écrire quelques recettes supplémentaires et notamment une recette de configuration de Rauc et une recette de création des paquets de mise à jour (bundles). Contrairement à Swupdate et Mender, il n'existe pas à ce jour de meta-layer d'exemple pour Rauc.

 

Tableau comparatif

Le tableau ci-dessous récapitule les différentes fonctionnalités proposées par chacun de ces trois systèmes de mise à jour OTA. Une colonne est aussi réservée pour OSTree, un autre système d'OTA dont l'utilisation est similaire à celle de Git.

Comparatif Systèmes OTA
  Mender Swupdate Rauc OSTree (non testé)
Site web mender.io sbabic.github.io rauc.io ostree.readthedocs.io
License Apache-2.0 GPL-2.0 LGPL-2.1 LGPL-2.0
Système de build Yocto (meta-mender) Yocto (meta-swupdate, meta-swupdate-boards)
Buildroot
Yocto (meta-rauc)
PTXdist
Yocto (meta-updater)
Type de mise à jour supporté Symétrique Symétrique et asymétrique Symétrique et asymétrique  Symétrique
Origine de la mise à jour Locale (ex : clé USB)
Distante (depuis une URL ou le serveur ‘mender-integration’)
Locale (ex : clé USB)
Distante (requête HTTP(S) ou un serveur dédié comme Hawkbit)
Locale (ex : clé USB)
Distante (depuis une URL ou un serveur dédié comme Hawbit)
Dépôt local et distant
Fichiers à mettre à jour Tout (rootfs+kernel) Flexible Flexible Tout (rootfs+kernel)
Structure du disque Au moins 4 partitions (boot, rootfsA, rootfsB, data) Flexible (rootfs A/B, initramfs, partition de recovery …) Flexible (rootfs A/B, initramfs, partition de recovery …) Flexible
Taille du binaire du client (Rpi) 4400ko 400ko 102ko 650ko
Bootloaders supportés U-boot Grub
U-boot
Grub
Barebox
U-boot
Grub
U-boot
Sécurité (installation) Rollback intégré Pas de rollback intégré (exemple disponible pour U-boot dans meta-swupdate-boards) Pas de rollback intégré (exemple disponible pour U-boot et Grub sur le dépôt de Rauc) Rollback intégré
Sécurité (communication) HTTPS obligatoire HTTPS possible HTTPS, SSH, … (tous ceux supportés par libcurl)  HTTPS possible
Sécurité (images) Images signées (RSA 3072+, ECDSA avec P-256) Images signées (RSA, CMS) et cryptées (AES-256/CBC) Images signées (x509) Commits signés (GPG)
Difficulté d’intégration Facile (support
BSP et config
de mender)
Moyenne (à faire avec yocto : BSP, recette pour intégrer le rollback, recette pour créer les packages de mise à jour) moyenne (à faire avec yocto : BSP, recette pour intégrer le rollback, recette pour créer les packages de mise à jour, recette pour intégrer Rauc-Hawkbit si utilisé)  N/A
Modularité Pre/post-install scripts
State scripts (plus flexibles)
Pre/post-install scripts
Handlers (ajout de support pour l’installation d’image)
Handlers (pre-install, install, post-install)
Hooks (applicables à toute les phases du process de màj)
 N/A
API coté serveur Requêtes HTTPS (RESTful API) Dépend du serveur
Avec hawkbit : librairies en Java
Dépend du serveur
Avec hawkbit : librairies en Java
Avec le projet pulp (plugin dispo pour ostree)
API coté client Requêtes HTTPS (RESTful API) Librairie en C utilisant les sockets UNIX Outil CLI ou interface D-Bus Outil CLI ressemblant à git ou librairie en C
Support https://groups.google.com/a/lists.mender.io/forum/#!forum/mender https://groups.google.com/forum/#!forum/swupdate https://www.mail-archive.com/rauc@pengutronix.de/  N/A

 

Exemple de configuration de système de mise à jour OTA

Afin d’avoir un exemple d’utilisation d’un système de mise à jour OTA, lançons-nous dans un petit cas d'étude. Nous allons mettre au point une petite application ‘Hello World’ qui se contentera d’afficher dans un terminal le message ‘Hello world from X.Y’ où X.Y représente la version actuelle du logiciel. Afin de déployer la mise à jour, nous nous appuierons sur un client Rauc ainsi que sur un serveur Hawkbit. Nous utiliserons le projet Yocto/OpenEmbedded pour compiler nos images. La plateforme utilisée dans ce déploiement est une Raspberry Pi modèle 1 B+. Le système de mise à jour devra être symétrique, l'image devra donc contenir deux rootfs et les mettre à jour alternativement. Un rootfs sera sur la partition /dev/mmcblk0p2 et l'autre sur /dev/mmcblk0p3.

Avant de rentrer dans le vif du sujet, il est important de dire quelques mots sur le projet Yocto. Sur le site www.yoctoproject.org, Yocto est défini comme étant un projet open-source collaboratif qui met a disposition des templates, des outils et des méthodes pour créer des distributions logicielles basées sur Linux destinées aux systèmes embarqués.

Dans un projet utilisant Yocto, les différents composants formant le système Linux sont définis par des recettes (‘recipes’) qui définissent comment les compiler et les installer sur le système de fichier cible. Ces recettes sont ensuite regroupées en couche (‘layers’) en fonction de leur champ d’application. Par exemple, le layer meta-raspberrypi contient toutes les recettes nécessaires à l'exécution du système Linux sur Raspberry Pi. Afin de générer le système, nous faisons appel à Bitbake. Ce logiciel écrit en Python représente le ‘chef cuisinier’ de Yocto : il examine toutes les recettes incluses dans notre projet, puis construit et installe les paquets correspondants. L’avantage principal de Yocto est qu’il gère la compilation croisée ainsi qu’une multitude de plateformes cibles.

Pour avoir plus d'informations à propos de Yocto, n’hésitez pas à consulter l’article ‘Yocto : comprendre BitBake’ sur le blog Linux Embedded.

 

Application de test

Notre application 'Hello world' est très simple. Elle peut être écrite sous la forme suivante :

#include <stdio.h>

int main(void) { 
    printf("Hello world v1.0 !\r\n"); 
    return 0; 
}

Pour simuler une mise à jour du logiciel, nous viendrons juste modifier le numéro de version affiché.
Pour intégrer ce programme dans Yocto, il faut simplement fournir une recette indiquant comment compiler et installer le binaire :

do_compile() { 
    ${CC} ${THISDIR}/files/helloworld.c -o hello ${LDFLAGS}  
} 

do_install() { 
    install -d ${D}${bindir} 
    install -m 0755 hello ${D}${bindir} 
}

 

Configuration de Rauc

La configuration de Rauc se fait par un fichier appelé system.conf. Ce fichier doit contenir un certain nombre d'entrées. Pour notre cas d'utilisation, le fichier présenté ci-dessous est suffisant :

[system]
compatible=RpiHello
bootloader=uboot

[keyring]
path=/etc/rauc/ca.cert.pem

[handlers]
post-install=/usr/lib/rauc/post-install.sh

[slot.rootfs.0]
device=/dev/mmcblk0p2
type=ext4
bootname=A

[slot.rootfs.1]
device=/dev/mmcblk0p3
type=ext4
bootname=B

Notre image sera identifiée par la chaîne contenue dans l'entrée 'compatible'. Ainsi, une mise à jour de type 'RpiHello' ne pourra être effectuée que sur des systèmes compatibles 'RpiHello'. Nous affectons aussi le bootloader à la valeur 'uboot'.

Dans la section 'keyring' se trouve le chemin vers le certificat une fois installé sur la cible.

Les deux partitions à mettre à jour sont décrites par des sections 'slots'. Chaque slot doit contenir le nom de l'appareil, son type (nous choisissons ici le format ext4) ainsi qu'un nom qui sera utilisé dans un script s'occupant de faire le basculement de rootfs.

Afin que ce fichier system.conf soit pris en compte par Yocto, vous devrez faire une recette dans votre layer (dans recipes-core) comportant notamment les éléments suivants :

rauc/
├── files/
│   ├── ca.cert.pem
│   ├── post-install.sh
│   └── system.conf
└── rauc_%.bbappend

Le fichier rauc_%bbappend devra contenir l'emplacement des différents fichiers contenus dans files/ et ajouter leur installation sur le système de fichier :

FILESEXTRAPATHS_prepend := "${THISDIR}/files:"

SRC_URI_append := "file://system.conf \
                   file://ca.cert.pem \
                   file://post-install.sh \
                  "

do_install_append () {
install -d ${D}${libdir}/rauc
install -m 0755 ${WORKDIR}/post-install.sh ${D}${libdir}/rauc/post-install.sh
}

Par défaut Rauc ne redémarre pas le système à la fin d'une mise à jour. Il est donc nécessaire de faire appel à un script de post-installation dans lequel nous programmons un redémarrage système.

La génération du fichier ca.cert.pem sera expliquée dans la partie suivante.

 

Génération des clés et des certificats

Afin d'assurer la sécurité de nos mises à jour, il est nécessaire d'utiliser un système de certificat qui permet de vérifier l'intégrité et l'authenticité des données transmises avec les mises à jour. Dans ce but Rauc dispose d'un script, openssl-ca.sh,  qui s'occupe de générer les différents certificats et clés de chiffrement. Vous trouverez ce script dans meta-rauc/scripts/.

Pour l'exécuter, créez un dossier à l'emplacement de votre choix (pas nécessairement dans Yocto), puis exécutez ce script depuis ce dossier. Un sous-dossier 'openssl-ca/' est généré avec l'arborescence suivante :

openssl-ca/
├── dev/
│   ├── ca.cert.pem
│   ├── ca.csr.pem
│   ├── certs/
│   │   ├── 01.pem
│   │   └── 02.pem
│   ├── development-1.cert.pem
│   ├── development-1.csr.pem
│   ├── index.txt
│   ├── index.txt.attr
│   ├── index.txt.attr.old
│   ├── index.txt.old
│   ├── private/
│   │   ├── ca.key.pem
│   │   └── development-1.key.pem
│   ├── serial
│   └── serial.old
└── openssl.cnf

Les fichiers qui nous intéressent sont en rouge.

Afin de pouvoir signer et vérifier les mises à jour, vous aurez à :

  • Copier le certificat 'ca.cert.pem' dans le dossier 'recipes-core/rauc/files/'
  • Créer une autre recette appelée 'bundles' qui permettra de générer les archives contenant tous les fichiers/rootfs mis à jour et y copier les fichiers development-1.cert.pem et private/development-1.key.pem. Vous devrez avoir les fichiers suivants dans cette recette :
    bundles/
    ├── files/
    │   ├── development-1.cert.pem
    │   └── development-1.key.pem
    └── update-bundle.bb

    Le fichier update-bundle.bb doit définir au minimum les variables suivantes :

    inherit bundle
    RAUC_BUNDLE_COMPATIBLE = "RpiHello"
    RAUC_BUNDLE_SLOTS = "rootfs"
    RAUC_SLOT_rootfs = "core-image-minimal"
    RAUC_SLOT_rootfs[fstype] = "ext4"
    RAUC_KEY_FILE = "${THISDIR}/files/development-1.key.pem"
    RAUC_CERT_FILE = "${THISDIR}/files/development-1.cert.pem"

 

Configuration du client Rauc-Hawkbit

Pour que le client Rauc puisse communiquer avec un serveur Hawkbit, il est nécessaire de mettre en place une passerelle entre ces deux systèmes. Ce logiciel est fournit par Rauc et téléchargeable à l'adresse https://github.com/rauc/rauc-hawkbit. Il est écrit en python (3.x) et nécessite donc d'inclure l’interpréteur python sur le système embarqué. Cependant, ce client Hawkbit ne fait que se servir de l'interface dbus de Rauc et envoyer des requêtes HTTP(s) au serveur. Il est donc possible de le réécrire en C/C++ si la taille mémoire est un enjeux important dans votre application.

Configurer ce client n'est pas chose aisée. Il n'existe aucun layer ou recette Yocto qui nous faciliterait son intégration. Si vous voulez l'utiliser dans votre projet, il va vous falloir concevoir quelques recettes. Voici les étapes les plus importantes :

  • créer une recette héritant de setuptool3 (pour installer des modules python3), qui télécharge Rauc-Hawkbit depuis Github et qui installe sur l'image le fichier de configuration du client Hawkbit :
    [client]
    hawkbit_server = votre_ip:votre_port
    ssl = false
    ca_file =
    tenant_id = DEFAULT
    target_name = test-target
    auth_token = ahVahL1Il1shie2aj2poojeChee6ahShu
    mac_address = ff:ff:ff:ff:ff:ff
    bundle_download_location = /tmp/bundle.raucb
    log_level = debug

    Un point important à noter est que Hawkbit requiert un identifiant unique (target_name) pour chaque appareil. Or au moment de la compilation de l'image, nous ne sommes pas en mesure de donner un quelconque identifiant (il en va de même pour l'adresse mac qui est fixée à ff:ff:ff:ff:ff:ff). En effet, l'image peut être potentiellement flashée sur des milliers de systèmes embarqués. Si nous fixions les identifiants maintenant, ils seraient les mêmes pour tous ces systèmes.
    Un moyen de contourner ce problème est d'exécuter un script au démarrage de la machine qui construit un identifiant unique (en utilisant l'adresse MAC de l'interface eth0 par exemple), puis remplace dans le fichier de configuration l'identifiant par défaut par l'identifiant tout juste construit. Voici un exemple de script qui pourrait remplir ce rôle :

    MAC=$(echo $(ip addr show eth0 | grep link/ | awk '{ print $2}'))
    grep -l 'target_name = *' /etc/rauc-hawkbit.cfg | xargs sed -i "s/test-target/rpihello_$MAC/"
  • créer une recette pour intégrer gbulb à Yocto. Gbulb est un module python qui, à la date de rédaction de cet article, ne dispose pas de recette Yocto. Cette recette pourrait être organisée de la manière suivante :
    recipes-devtools/
    └── python/
        ├── python3-gbulb_0.2.bb
        └── python-gbulb.inc

    Avec les fichiers suivants :

    recipes-devtools/python/python-gbulb.inc :

    SUMMARY = "Gbulb"
    DESCRIPTION = ""
    HOMEPAGE = "https://github.com/nathan-hoad/gbulb"
    LICENSE = "Apache-2.0"
    LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/Apache-2.0;md5=89aea4e17d99a7cacdbeed46a0096b10"
    
    inherit pypi
    
    SRC_URI[md5sum] = "bf3f2f1de2606f1310111e3479cc52d2"
    SRC_URI[sha256sum] = "5e40bf0354f3fc31d1a5e701dae6afc96f1a1bb432ce44de2d2ad3e62ce6e173"
    
    RDEPENDS_${PN} += " \
                       ${PYTHON_PN}-asyncio \
                       ${PYTHON_PN}-pygobject \
                      "

    recipes-devtools/python/python3-gbulb_0.2.bb :

    inherit setuptools3
    require python-gbulb.inc
  • créer un service systemd pour que le client Hawkbit s'exécute au démarrage du système. Pour cela, il est nécessaire de créer une recette qui hérite de systemd et qui se charge d'installer deux fichiers sur l'image : le script à exécuter ainsi que le 'unit file' (fichier *.service). Il serait même approprié de mettre dans le script de démarrage les quelques lignes de code qui permettent de déterminer un identifiant unique.

 

Déploiement de la mise à jour

Une fois toutes les recettes créées, vous pouvez compiler l'image et la flasher sur votre système embarqué. Comme mentionné plus haut, pour faire une mise à jour de notre application de test, nous incrémentons le numéro de version affiché. Il suffit alors de recompiler l'image et de télécharger le bundle généré par Rauc (fichier *.raucb) sur votre serveur Hawkbit. Ce transfert peut se faire directement depuis l'interface graphique dont voici un aperçu :

Avant le téléchargement, il vous est demandé de créer un 'Software module'. On peut créer autant de modules que l'on a de logiciels différents à mettre à jour. Dans le cadre de notre étude, nous nous contentons de créer un seul module dont le logiciel à mettre à jour est le rootfs entier. Il vous faut aussi créer une distribution à laquelle vous affectez tous les software modules représentant les logiciels appartenant à cette distribution.

Les cibles à mettre à jour peuvent être sélectionnées via un système de filtres. Dans notre cas, nous pouvons avoir un parc de Raspberry Pi dotées de l'application Hello dont l'identifiant est de la forme rpihello_<adresse_mac>. Ainsi, nous créons un filtre qui donne les appareils dont le nom est de la forme 'rpihello_*'.

Une fois cela fait, il suffit d'aller dans l'onglet 'Rollout' pour créer un nouveau déploiement. Sélectionnez la nouvelle distribution à flasher, le filtre, les différents modes de mise à jour (instantanée, action requise sur le client, planification, etc).

 

Conclusion

Utilisés depuis plusieurs années dans la téléphonie mobile, les mécanismes de mise à jour Over-The-Air deviennent de plus en plus utilisés dans les domaines des systèmes embarqués et de l'Internet des Objets. Les problématiques de fiabilité et de sécurité en sont les enjeux majeurs. Concernant la mise à jour de systèmes Linux, plusieurs solutions existent. Chacune d'elle apporte ses avantages et ses inconvénients, offrant ainsi une plus ou moins grande flexibilité au prix d'une certaine complexité dans la configuration de ce système. Il est important de choisir sa solution en faisant attention à toutes ses fonctionnalités, afin qu'elle réponde au mieux aux besoins de notre application.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.