Expérimentation de Fedora CoreOS sur Proxmox : création de la machine virtuelle d'installation

Ce billet est le deuxième d’une série traitant de la création d’une infrastructure virtualisée à l’aide de Proxmox1 pour la partie hyperviseur et de Fedora CoreOS2 pour le système d’exploitation des machines virtuelles invitées (guests). L’infrastructure codifiée (Infrastructure as Code) est réalisée avec OpenTofu3 (Hashicorp Terraform ayant rejoint le côté obscur de la Force).

Dans le premier billet4, nous avons évoqué les raisons qui ont mené au choix de créer une machine virtuelle d’installation. Dans ce billet, nous allons voir une solution technique preuve de concept satisfaisant ce besoin. Le code source est publié en licence ouverte, afin que la communauté puisse en bénéficier 5.

L’objectif est ici d’installer une machine virtuelle (sur Proxmox) qui permettra d’en installer d’autres. La distribution Fedora CoreOS est utilisée pour ses atouts de stabilité et de sécurité intrinsèques (et détaillés dans le premier billet).

Sur cette distribution, nous allons déployer un serveur DHCP qui permettra le démarrage par le réseau des futures machines de l’infrastructure. Ce serveur DHCP fournira un script iPXE6, une version moderne et en source ouverte de PXE (Preboot eXecution Environment), ainsi que des options DHCP distinctes en fonction de la machine à installer. Ces options permettront notamment de fournir une adresse réticulaire (URL) de type HTTP, afin de récupérer un fichier de configuration Ignition spécifique à chaque machine à installer. Ce fichier Ignition permettra la personnalisation de cette machine à partir de l’image Fedora Core OS générique.

Le serveur HTTP est le second service de cette machine virtuelle d’installation. Il permet la publication des configurations Ignition, mais aussi du script iPXE à exécuter, et de l’image de Fedora CoreOS à utiliser pour l’installation.

Le dernier service de cette machine d’installation est un serveur de fichiers sur lequel Opentofu (ou Terraform si vous y tenez vraiment) pourra déposer des extensions de configuration DHCP et des fichiers Ignition : un pour chaque machine virtuelle de l’infrastructure à installer. Dans cette preuve de concept, le serveur de fichiers est un serveur SFTP. Nous verrons dans la suite de cet article que c’est un choix par défaut, et qui n’est pas entièrement satisfaisant.

Déploiement de services sur Fedora CoreOS

Extensions par conteneurs

Fedora CoreOS est une distribution optimisée pour l’hébergement de conteneurs. Son socle (constitué par les namespaces7 par défaut) ne contient que très peu de programmes, et les interactions entre les différents logiciels le composant ainsi qu’avec les conteneurs sont fortement limitées par le durcissement mis en place, notamment avec SELinux.

Cette distribution permet bien d’installer des logiciels additionnels sur le socle, grâce à un système de surcouches (layering) de rpm-ostree8. Cette éventualité doit cependant être écartées dans le cas de cette machine virtuelle d’installation ; en effet, celle-ci démarre sur un LiveCD ISO et la propriété d’immuabilité de Fedora CoreOS “interdit” l’ajout de nouveaux programmes sur le système en cours d’exécution.

L’ensemble des services déployés doivent donc l’être grâce à des conteneurs. Ce n’est pas si différent de ce qui doit être fait dans un cluster Kubernetes, et il n’est pas étonnant que Red Hat CoreOS soit utilisée comme socle pour la plateforme OpenShift9.

De même, les volumes des conteneurs ne sont qu’assez rarement des bind-mount10 de répertoires arbitraires du socle, et plus généralement des volumes (anonymes ou nommés) du moteur de conteneurs. Cela est dû aux politiques SELinux11 qui restreignent les interactions avec les fichiers dans les volumes, qui doivent avoir (principalement) les types container_file_t ou container_ro_file_t12, ce qu’ont rarement les fichiers du socle.

Ainsi, le service DHCP et le service HTTP sont déployés sous la forme de conteneurs, et en conséquence, le serveur de fichiers doit l’être également, puisqu’il sert à ajouter du contenu aux volumes exposés aux deux premiers services.

Une autre conséquence est que si un conteneur doit disposer de fichiers pré-existants dans des volumes, il faut trouver un moyen de les y placer. La méthode la plus propre et la plus simple est d’utiliser des conteneurs d’initialisation13, à l’instar de ce qui est fait dans les déploiements Kubernetes. Podman dispose également de la commande podman kube qui permet d’émuler en partie les déploiements Kubernetes et notamment les objets ConfigMap14. Les ConfigMaps étant plus “limitées” que le dépôts de fichiers arbitraires, il a été choisi pour cette preuve de concept d’utiliser exclusivement des conteneurs d’initialisation.

Les Podman Quadlets

Fedora CoreOS dispose par défaut des moteurs Moby (Docker) et Podman.

S’il est bien sûr possible d’utiliser compose pour définir les conteneurs, les orchestrer et les lancer, il est également possible d’utiliser les Podman Quadlets15. Les Quadlets sont des fichiers de configuration ressemblant à des unités systemd (systemd units), avec des sections en plus. Il en existe de plusieurs types : conteneurs, pods, images, volumes, réseaux, et même une couche de compatibilité avec la syntaxe Kubernetes.

Voici par exemple le quadlet pour le volume stockant les baux DHCP de notre preuve de concept, stocké dans le fichier /etc/containers/systemd/dhcp_data.volume :

[Unit]
Description = DHCP Data Volume

[Volume]
VolumeName = dhcp_data
Device=/dev/disk/by-label/dhcp_data
Options=nodev,noexec,nosuid,rootcontext=system_u:object_r:container_file_t:s0
Type=ext4

Le quadlet du conteneur d’initialisation téléchargeant la dernière image de Fedora CoreOS est :

[Unit]
Description = Download Latest FCOS Image

[Container]
ContainerName = fcos_downloader
Image = image_downloader.image
Exec = download -s stable -f pxe
Volume = fcos_images.volume:/data:z
WorkingDir = /data

[Install]
WantedBy=multi-user.target

Ils sont consommés par un générateur de services systemd qui transforme ces quadlets, situés dans /etc/containers/systemd, en services, situés dans /run/systemd/generator.

L’un des intérêts des quadlets est leur intégration au sein de systemd, avec la possibilité de contrôler de manière assez fine les dépendances avec d’autres services.

En outre, leur syntaxe permet également de monter automatiquement des systèmes de fichiers sur des volumes. Couplé à la facilité de créer des partitions avec Ignition, il est ainsi trivial de créer une partition par volume. Il s’agit d’une bonne pratique de sécurité qui permet de limiter les risques de dénis de service ou de corruption entre conteneurs par saturation d’un système de fichiers commun à plusieurs volumes.

Difficultés rencontrées

Un projet ne serait pas une aventure, sans son lot d’ornières et d’imprévus. Quelle aventure se fut !

Problèmes avec le fournisseur Ignition d’Opentofu

Les fichiers de configuration Ignition sont des documents JSON.

Chaque document JSON décrit une machine complète : partitions, systèmes de fichiers, fichiers de configuration systemd, fichiers arbitaires, répertoires, utilisateurs et groupes, etc. Le contenu des fichiers à créer est sérialisé dans ce document JSON, soit verbatim, soit sous la forme d’une adresse de type data:16.

Il est possible qu’un fichier de configuration Ignition contienne des directives d’inclusion/fusion d’autres fichiers Ignition pouvant être récupérés notamment par le réseau. Il faut dans ce cas s’assurer de l’intégrité de ces fichiers distants, afin de prévenir la compromission totale du serveur.

Fedora CoreOS étant un système d’exploitation immuable, reposant sur des images systèmes administrées par rpm-ostree, l’image de base est la même pour toutes les machines d’une infrastructure virtualisée ; les machines se diversifient et se spécialisent via leur configuration Ignition. Cette approche est donc opposée à celles impliquant la pré-personnalisation des images avec des outils comme Packer17.

Le moment et la manière de générer la configuration Ignition peut varier en fonction des préférences individuelles. Le plus gros de la configuration peut être statique : tous les serveurs HTTP ont besoin des mêmes images de conteneurs, des mêmes scripts de démarrage, des mêmes partitions et volumes pour stocker certaines informations de manière persistante. Tout cela peut être stocké dans un fichier Ignition commun à toutes les instances et ultérieurement inclus, ou être mis dans le fichier Ignition de chaque instance. C’est au choix. Avec une approche cloud-init, ces fichiers feraient parties de l’image générée avec Packer.

En revanche, le contenu servi par ces serveurs HTTP ou l’adresse IP d’écoute sont des informations spécifiques à chaque instance. Avec l’approche cloud-init, ces informations seraient communiquées au système d’exploitation par le service de métadonnées interrogé par l’utilitaire cloud-init.

Dans le cas d’espèce, puisque nous n’avons pas encore d’infrastructure virtualisée, pas de serveur HTTP de confiance où stocker et distribuer la configuration commune à toutes les machines virtuelles d’installation, et que le fichier Ignition va être stocké dans un ISO personnalisé de Fedora CoreOS, nous n’allons générer qu’un seul fichier Ignition, contenant les informations communes à plusieurs instances éventuelles et les informations spécifiques à une instance spécifique.

Concernant la conception du document JSON au format Ignition, nous pourrions écrire cette configuration Ignition avec n’importe quel outil, y compris “manuellement”, mais ultérieurement, nous le ferons avec Opentofu, au moins pour les informations spécifiques. Par cohérence, Opentofu a donc également été utilisé pour cette machine.

Étant donné que les fichiers Ignition sont exprimés en JSON, il est possible de structurer ses données en HCL (HashiCorp Configuration Language), puis de faire appel à la fonction jsonencode. Pour autant, Hashicorp a initialement développé un fournisseur Terraform pour Ignition18, qui a ensuite été abandonné, puis repris par la communauté19. Ce fournisseur propose des sources de données (data sources) afin de structurer la configuration Terraform et la typer.

Ce fournisseur est assez basique puisqu’il se contente de codifier sous la forme d’un schéma de source de données les champs des différentes structures définies dans la spécification d’Ignition20.

Avec le fournisseur, on écrit donc :

data "ignition_file" "dnsmasq_container" {
    path = "/etc/containers/systemd/dnsmasq.container"
    mode = 420
    content {
        content = file("${path.module}/files/dnsmasq.container")
    }
}

data "ignition_config" "example" {
    files = [
        data.ignition_file.dnsmasq_container.rendered,
    ]
}

locals {
    serialized_config = data.ignition_config.example.rendered
}

En HCL “pur”, pour comparaison, on écrit :

locals {
    dnsmasq_container_file = {
        path = "/etc/containers/systemd/dnsmasq.container"
        mode = 420
        contents = {
            source = format(
                "data:text/plain;base64,%s",
                base64encode(file("${path.module}/files/dnsmasq.container"))
            )
        }
    }

    serialized_config = jsonencode({
        ignition = {
            version = "3.4.0"
        }
        storage = {
            files = [
                local.dnsmasq_container_file,
            ]
        }
    })
}

La différence de verbosité entre les deux versions n’est pas flagrante ; mais surtout, le fournisseur comporte plusieurs problèmes : le document JSON généré n’est pas minimal, certaines générations sont incorrectes, et il manque des options pourtant spécifiées dans la version 3.4.0 du format Ignition.

La génération non-minimaliste est dû à l’usage par le fournisseur des types définis dans le code source de l’outil Ignition lui-même21. Par exemple la structure racine d’une configuration Ignition est définie ainsi :

type Ignition struct {
	Config   IgnitionConfig `json:"config,omitempty"`
	Proxy    Proxy          `json:"proxy,omitempty"`
	Security Security       `json:"security,omitempty"`
	Timeouts Timeouts       `json:"timeouts,omitempty"`
	Version  string         `json:"version"`
}

La plupart des champs sont définis avec l’annotation de sérialisation JSON omitempty. Quand cette annotation est utilisée de manière correcte, le champ n’apparait pas dans le document JSON généré avec la fonction json.Marshal si sa valeur est “fausse” ou “vide” selon le système de typage du langage Go. Or, les sous-structures ne sont pas définies comme des pointeurs. Ainsi, elles ne peuvent jamais être “vides”, et les champs sont toujours ajoutés au document JSON, même si elles ne contiennent aucune valeur.

Voici la configuration générée par le code ci-dessus utilisant le fournisseur :

{
  "ignition": {
    "config": {
      "replace": {
        "verification": {}
      }
    },
    "proxy": {},
    "security": {
      "tls": {}
    },
    "timeouts": {},
    "version": "3.4.0"
  },
  "kernelArguments": {},
  "passwd": {},
  "storage": {
    "files": [
      {
        "group": {},
        "overwrite": false,
        "path": "/etc/containers/systemd/dnsmasq.container",
        "user": {},
        "contents": {
          "source": "data:text/plain;charset=utf-8;base64,W1VuaXRdCkRlc2NyaXB0aW9uID0gREhDUCBDb250YWluZXIKCldhbnRzPWltYWdlX2Rvd25sb2FkZXIuc2VydmljZQpBZnRlcj1pbWFnZV9kb3dubG9hZGVyLnNlcnZpY2UKV2FudHM9bmV0d29yay1vbmxpbmUudGFyZ2V0CkFmdGVyPW5ldHdvcmstb25saW5lLnRhcmdldApXYW50cz1kaGNwX2NvbmZpZ19pbml0LnNlcnZpY2UKQWZ0ZXI9ZGhjcF9jb25maWdfaW5pdC5zZXJ2aWNlCgpbQ29udGFpbmVyXQpDb250YWluZXJOYW1lID0gZG5zbWFzcV9jb250YWluZXIKSW1hZ2UgPSBsb2NhbGhvc3QvZG5zbWFzcTpsYXRlc3QKVm9sdW1lID0gZGhjcF9jb25maWcudm9sdW1lOi9ldGMvZG5zbWFzcS5kOnoKVm9sdW1lID0gZGhjcF9kYXRhLnZvbHVtZTovZGF0YTpaClZvbHVtZSA9IC9kZXYvbG9nOi9kZXYvbG9nCk5ldHdvcmsgPSBob3N0CkFkZENhcGFiaWxpdHkgPSBDQVBfTkVUX0FETUlOLENBUF9ORVRfUkFXCgpbU2VydmljZV0KV29ya2luZ0RpcmVjdG9yeT0vdmFyL3Jvb3Rob21lL2RoY3AKRXhlY1N0YXJ0UHJlPS9iaW4vYmFzaCAvdmFyL3Jvb3Rob21lL2dlbmVyYXRlX2RoY3Bfb3B0aW9ucy5zaApFeGVjU3RhcnRQcmU9L3Vzci9iaW4vcG9kbWFuIGJ1aWxkIC10IGRuc21hc3E6bGF0ZXN0IC4KUmVzdGFydD1vbi1mYWlsdXJlCgpbSW5zdGFsbF0KV2FudGVkQnk9bXVsdGktdXNlci50YXJnZXQKCg==",
          "verification": {}
        },
        "mode": 420
      }
    ]
  },
  "systemd": {}
}

En comparaison, voici le document JSON généré en écrivant soi-même en HCL la configuration Ignition :

{
  "ignition": {
    "version": "3.4.0"
  },
  "storage": {
    "files": [
      {
        "contents": {
          "source": "data:text/plain;base64,W1VuaXRdCkRlc2NyaXB0aW9uID0gREhDUCBDb250YWluZXIKCldhbnRzPWltYWdlX2Rvd25sb2FkZXIuc2VydmljZQpBZnRlcj1pbWFnZV9kb3dubG9hZGVyLnNlcnZpY2UKV2FudHM9bmV0d29yay1vbmxpbmUudGFyZ2V0CkFmdGVyPW5ldHdvcmstb25saW5lLnRhcmdldApXYW50cz1kaGNwX2NvbmZpZ19pbml0LnNlcnZpY2UKQWZ0ZXI9ZGhjcF9jb25maWdfaW5pdC5zZXJ2aWNlCgpbQ29udGFpbmVyXQpDb250YWluZXJOYW1lID0gZG5zbWFzcV9jb250YWluZXIKSW1hZ2UgPSBsb2NhbGhvc3QvZG5zbWFzcTpsYXRlc3QKVm9sdW1lID0gZGhjcF9jb25maWcudm9sdW1lOi9ldGMvZG5zbWFzcS5kOnoKVm9sdW1lID0gZGhjcF9kYXRhLnZvbHVtZTovZGF0YTpaClZvbHVtZSA9IC9kZXYvbG9nOi9kZXYvbG9nCk5ldHdvcmsgPSBob3N0CkFkZENhcGFiaWxpdHkgPSBDQVBfTkVUX0FETUlOLENBUF9ORVRfUkFXCgpbU2VydmljZV0KV29ya2luZ0RpcmVjdG9yeT0vdmFyL3Jvb3Rob21lL2RoY3AKRXhlY1N0YXJ0UHJlPS9iaW4vYmFzaCAvdmFyL3Jvb3Rob21lL2dlbmVyYXRlX2RoY3Bfb3B0aW9ucy5zaApFeGVjU3RhcnRQcmU9L3Vzci9iaW4vcG9kbWFuIGJ1aWxkIC10IGRuc21hc3E6bGF0ZXN0IC4KUmVzdGFydD1vbi1mYWlsdXJlCgpbSW5zdGFsbF0KV2FudGVkQnk9bXVsdGktdXNlci50YXJnZXQKCg=="
        },
        "mode": 420,
        "path": "/etc/containers/systemd/dnsmasq.container"
      }
    ]
  }
}

Cette définition incorrecte des types dans le code source d’Ignition n’est pas trivial à corriger car ces types sont générés par un outil appelé schematyper22. Cet outil prend en entrée une description de données au format JSON Schema23, et écrit en sortie des définitions de structures en Go.

Dans le cas d’espèce, cette verbosité excessive n’est pas forcément gênante car nous n’avons pas de contrainte de taille de fichiers. Certains services de metadonnées cloud en ont cependant une (généralement autour de 16ko) ; dans ces cas, la taille compte.

Également, bien que les champs soient bien définis par le code source d’Ignition, le fournisseur Terraform est incomplet et plusieurs définitions manquent, rendant impossible l’utilisation de certaines options. Ce n’est pas que c’est difficile à ajouter… mais leur absence cumulée au fait qu’exprimer en HCL une configuration Ignition fait qu’il devient immédiatement préférable de s’en passer.

Problèmes avec la prise en charge de SFTP par Opentofu

La solution proposée utilise SFTP afin de permettre l’extension de la configuration du serveur DHCP et la publication de fichiers Ignition pour les futures machines virtuelles qui composeront l’infrastructure.

SFTP est un service natif d’OpenSSH, un des démons les plus exposés et les plus sensibles puisque sa présence est quasi universelle et qu’il transporte notamment les flux d’administration. Son emploi est particulièrement intéressant pour le type de transfert de fichiers utilisé dans cette preuve de concept, grâce à son chiffrement de flux par défaut, son identification native par clé publique, et ses capacités d’isolation à l’aide de chroot24.

De surcroit, la configuration est triviale :

Subsystem sftp internal-sftp
Match User terraform_ignition
ForceCommand internal-sftp
ChrootDirectory /my/chroot/path

Compte tenu des politiques de sécurité SELinux en place par défaut sur Fedora CoreOS, il n’est pas possible d’utiliser le processus OpenSSH Server du socle. En effet, lors de la réception des fichiers, ces derniers sont marqués avec le type SELinux user_home_t avec lequel les conteneurs ne peuvent interagir.

Pour cette raison, la preuve de concept dispose de deux serveurs SSH : un pour se connecter au socle et un autre, accessible uniquement en SFTP, permettant le téléversement de fichiers. Ainsi, les fichiers déposés en SFTP sont marqués avec le type container_file_t qui peut être lu par d’autres conteneurs.

Hélas, ce superbe service SFTP ne peut être utilisé nativement par Opentofu… En effet, l’espoir était que les configurations DHCP soient générées par Opentofu, écrites sur disque à l’aide d’une ressource local_file25 ou même une null_resource26, puis téléversées grâce au mécanisme d’approvisionnement (provisioner) “file”27. Le code aurait ressemblé à :

resource "null_resource" "ignition_configuration" {
  provisioner "file" {
    content = local.encoded_config
    destination = "writable/${vm_id}.ign"  
    connection {
      type = "ssh"
      host = var.netboot_server_ip
      port = 2222
      user = "terraform_ignition"
      agent = true
      bastion_host = var.pve_host
      bastion_user = var.pve_pam_user
      bastion_port = 22
    }
  }
}

Il s’avère néanmoins que cela n’est pas possible, et une tentative renvoie le message d’erreur :

Upload failed: his service allows sftp connections only`

La documentation explique que :

Provisioners which execute commands on a remote system via a protocol such as SSH typically achieve that by uploading a script file to the remote system and then asking the default shell to execute it.

Ce mécanisme d’approvisionnement ayant pour but la copie de fichiers notamment par SSH n’est pas compatible avec le mécanisme standard de transfert de fichiers de SSH28

La preuve de concept actuelle repose donc sur plusieurs mécanismes d’approvisionnement, dont local-exec29 afin d’exécuter la commande sftp via le shell. Comme cette solution est un bricolage peu satisfaisant, l’auteur de cet article envisage de développer, dans un futur plus ou moins proche, un fournisseur Opentofu pour la gestion de ressources de type fichiers au travers du protocole WebDav30, afin de palier cette situation.

Problèmes avec le fournisseur Opentofu pour Proxmox

Le fournisseur Opentofu pour Proxmox bpg/proxmox31 est le fournisseur le plus avancé disponible sur les registres de Terraform.

Hélas, ce dernier ne dispose pas d’une ressource pour le téléversement de fichiers ISO. La ressource proxmox_virtual_environement_file permet certes le téléversement de fichiers arbitraires, mais comme l’indique la documentation, un transfert par SSH vers l’hyperviseur est impliqué, au lieu d’utiliser l’API de Proxmox. La surface d’attaque de cette ressource est donc bien plus importante que si l’API avait été utilisée. C’est d’autant plus regrettable que l’API fournit bien un moyen de téléverser des fichiers ISO.

En conséquence, dans cette preuve de concept, le mécanisme d’approvisionnement local-exec d’Opentofu a été utilisé afin de téléverser le fichier avec l’utilitaire curl par l’API de Proxmox :

    provisioner "local-exec" {
        command = <<EOT
curl \
  -F "content=iso" \
  -F "filename=@customized-${random_pet.config_name.id}.iso;type=application/vnd.efi.iso;filename=fcos-netboot-server-${random_pet.config_name.id}.iso" \
  -H "@${local_file.api_token.filename}" \
  "${var.pve_api_base_url}nodes/${var.pve_node_name}/storage/${var.pve_storage_id}/upload"
EOT      
    }

Une ressource de type local_file a également été utilisée pour stocker le jeton d’API, de manière à éviter d’exposer le jeton directement sur la ligne de commandes32.

Problèmes avec systemd.path et la détection de changements de fichiers dans un répertoire

Le serveur DHCP utilisé par cette preuve de concept repose sur dnsmasq. Celui-ci contient certainement moins de fonctionnalités que le serveur de l’ISC, mais il convient parfaitement pour le démarrage par le réseau.

Une fonctionnalité manquante sur ces deux logiciels est la capacité de recharger automatiquement la configuration lors de son changement ou de son extension. Or, cette preuve de concept repose sur l’extensibilité de la configuration par Opentofu : lorsqu’une nouvelle machine est ajoutée à l’infrastructure, des options DHCP sont définies dynamiquement pour permettre son installation. En particulier, une option DHCP indique où trouver le fichier de configuration Ignition spécifique à cette machine à installer et une autre indique le chemin du disque dur sur lequel effectuer l’installation.

dhcp-host=${mac_address},set:${hostname}tag,${host_ip},${hostname}
dhcp-option=tag:${hostname}tag,encap:128,2,"${vm_id}.ign"
dhcp-option=tag:${hostname}tag,encap:128,3,"/dev/disk/by-path/pci-0000:00:0a.0"

Le redémarrage du service pourrait être fait avec un mécanisme d’approvisionnement de type remote-exec33, mais cette solution nécessite la capacité d’exécuter des commandes sur le socle depuis Opentofu.

Opentofu dispose déjà de la capacité d’exécuter des commandes arbitraires sur toutes les machines de l’infrastructure par injection de commandes et de fichiers dans les configurations Ignition. Cela demande néanmoins le redémarrage de la machine virtuelle pour appliquer la nouvelle configuration, ce qui n’est pas forcément très discret pour un attaquant qui souhaiterait ainsi compromettre des machines.

Une solution plus propre et ne nécessitant pas d’accès shell est de surveiller les fichiers de configuration avec inotify34. Inotify est une fonctionnalité du noyau Linux qui permet d’émettre des événements lors d’opérations sur le système de fichiers. Les programmes intéréssés peuvent s’abonner à ces événements en vue d’y réagir.

Systemd peut être configuré pour surveiller le système de fichiers avec inotify, grâce aux unités de type path35. La configuration ressemble à ceci :

[Unit]
Description = Path Monitor for DHCP Config

[Path]
PathChanged=/path/to/monitored/path
TriggerLimitIntervalSec=0

[Install]
WantedBy=multi-user.target

Cette unité d’exemple active un service du même nom que celui de l’unité de type path lorsque le chemin indiqué subit une modification. Ce chemin peut être un fichier ou un répertoire. Dans le cas d’un répertoire, les changements n’interviennent que lors de la suppression ou de l’ajout d’un fichier ; les opérations de modification des fichiers contenus dans un répertoire n’entrainent pas de modification du répertoire lui-même.

Hélas, bien qu’il existe une directive PathExistsGlob permettant l’usage de chaines de substitution (e.g. *.conf), il n’existe aucune directive de type PathChangedGlob qui permettrait la surveillance de tous les fichiers au sein d’un répertoire. Un ticket à ce sujet est ouvert depuis plusieurs années36. Un bricolage pour s’accomoder de la situation est de supprimer les fichiers de configuration existants avant d’ajouter les nouveaux, de façon à provoquer un ou deux redémarrages du service37 et la prise en compte de la nouvelle configuration.

Un autre défaut de cette solution est qu’elle exige que systemd “espionne” les fichiers déposés par SFTP dans un volume géré par Podman. Cela implique de faire une hypothèse sur les chemins utilisés par Podman ou de monter une deuxième fois le système de fichiers associé à ce volume à un chemin connu du socle. Si le montage peut sembler plus propre, il faut considérer que lorsqu’on monte un même système de fichiers à de multiples endroits, il faut que les options de montage soient identiques, y compris les informations relatives à SELinux (e.g. l’option rootcontext). Il faut alors à nouveau faire des hypothèses : les options utilisées par les Quadlets Podman lors du montage du système de fichiers sur le volume.

Dans le ticket précédemment évoqué, un moyen de contournement est évoqué : utiliser l’utilitaire inotifywatch38. Ce programme permet la surveillance de plusieurs fichiers dans un répertoire, nommés suivant un motif spécifié. Hélas, ce dernier n’est pas disponible sur le socle de Fedora CoreOS, et comme expliqué précédemment, il n’est pas possible d’installer des logiciels additionnels dans notre cas d’usage. Il est cependant possible de l’utiliser dans un conteneur qui aurait également accès au volume des extensions de configuration DHCP. Une pierre, deux coups : on contournerait la limitation de systemd et on ne ferait plus d’hypothèse sur le chemin d’accès ! Hélas, il reste ensuite à trouver un moyen de redémarrer le service dnsmasq depuis le conteneur ayant détecté le changement…

Donner un accès SSH sur le socle depuis le conteneur de surveillance semble la voie royale. Hélas, cet accès est compliqué à donner, du fait des politiques SELinux, encore une fois, ou du plan d’adressage dynamique de la couche réseau de Podman.

L’hypothèse à faire sur le plan d’adressage de Podman est de connaitre l’adresse IP associée au socle. Elle doit être faite si on tente de se connecter en SSH au socle par le réseau. Une solution pour éviter de faire des hypothèses sur la couche réseau est d’établir la connexion SSH à travers une socket Unix bind-montée dans le conteneur de surveillance. Exposer un service SSH sur socket Unix à d’autres conteneurs ou utilisateurs est une astuce assez élégante notamment discutée par Timothée Ravier, développeur de CoreOS, sur son blog dans le cadre d’un remplacement de sudo par une connexion SSH locale39. Hélas, dans le cas d’espèce, les politiques SELinux empêchent la connexion de socat, utilisé par le client SSH du conteneur, à la socket Unix exposée par le serveur SSHD du socle.

En conséquence, sur cette problématique, aucune solution satisfaisante n’a été trouvée. La solution retenue est, en attendant de trouver mieux, d’utiliser une unité systemd de type path en faisant l’hypothèse sur le chemin du volume Podman, et en supprimant puis en rajoutant le fichier de configuration, afin de déclencher le redémarrage du service.

Conclusion

Cet article a couvert une partie des problèmes rencontrés et des solutions proposées pour la création d’une preuve de concept d’un serveur d’installation pour une infrastructure basée sur Fedora CoreOS sur Proxmox. Le code est ouvert5 et les commentaires sont les bienvenues, afin d’améliorer cette dernière et permettre à la communauté d’en bénéficier.

Grâce à cette machine d’installation, il devient trivial de déployer de nouvelles machines faisant tourner Fedora CoreOS. Il suffit pour cela de téléverser le fichier Ignition de la machine à installer et un fichier d’extension de la configuration de dnsmasq, puis de configurer la nouvelle machine virtuelle pour démarrer par le réseau.

Ces téléversements peuvent s’effectuer lors de la déclaration de la nouvelle machine virtuelle dans la configuration d’Opentofu.

Dans le prochain billet de cette série, nous verrons comment déployer, grâce à cette machine d’installation, un serveur DNS40, un serveur ACME41, et un cluster etcd42, en vue d’installer une instance d’Openbao43, et ainsi pouvoir enfin créer des instances de Fedora CoreOS sans placer de secrets dans les fichiers Ignition.


  1. https://www.proxmox.com/en/ ↩︎

  2. https://fedoraproject.org/coreos/ ↩︎

  3. https://opentofu.org/ ↩︎

  4. https://www.broken-by-design.fr/posts/proxmox-fcos1/ ↩︎

  5. https://git.broken-by-design.fr/fmaury/iac ↩︎ ↩︎

  6. https://ipxe.org/ ↩︎

  7. https://www.man7.org/linux/man-pages/man7/namespaces.7.html ↩︎

  8. https://coreos.github.io/rpm-ostree/ ↩︎

  9. https://www.redhat.com/fr/technologies/cloud-computing/openshift ↩︎

  10. https://docs.docker.com/storage/bind-mounts/ ↩︎

  11. https://github.com/containers/container-selinux/blob/main/container_selinux.8 ↩︎

  12. https://github.com/containers/container-selinux/blob/main/container.te ↩︎

  13. https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ ↩︎

  14. https://kubernetes.io/docs/concepts/configuration/configmap/ ↩︎

  15. https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html ↩︎

  16. https://www.rfc-editor.org/rfc/rfc2397 ↩︎

  17. https://www.packer.io/ ↩︎

  18. https://github.com/hashicorp/terraform-provider-ignition ↩︎

  19. https://github.com/community-terraform-providers/terraform-provider-ignition ↩︎

  20. https://coreos.github.io/ignition/configuration-v3_4/ ↩︎

  21. https://github.com/coreos/ignition/blob/v2.18.0/config/v3_4/types/schema.go ↩︎

  22. https://github.com/idubinskiy/schematyper ↩︎

  23. https://json-schema.org/ ↩︎

  24. chroot(2) est un appel système qui permet de restreindre la capacité d’un processus à changer de répertoires ; bien utilisé, il permet de restreindre les accès à un sous-ensemble de la hiérarchie des fichiers d’un système de fichiers sous Linux. ↩︎

  25. https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file ↩︎

  26. https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource ↩︎

  27. https://developer.hashicorp.com/terraform/language/resources/provisioners/file ↩︎

  28. Le protocole SCP est devenu obsolète à la suite d’une vulnérabilité protocolaire irréparable : https://lwn.net/Articles/835962/ ↩︎

  29. https://developer.hashicorp.com/terraform/language/resources/provisioners/local-exec ↩︎

  30. https://www.rfc-editor.org/rfc/rfc4918 ↩︎

  31. https://registry.terraform.io/providers/bpg/proxmox/latest/docs ↩︎

  32. https://cwe.mitre.org/data/definitions/214 ↩︎

  33. https://developer.hashicorp.com/terraform/language/resources/provisioners/remote-exec ↩︎

  34. https://www.man7.org/linux/man-pages/man7/inotify.7.html ↩︎

  35. https://www.freedesktop.org/software/systemd/man/latest/systemd.path.html ↩︎

  36. https://github.com/systemd/systemd/issues/14330 ↩︎

  37. L’incertitude entre un ou deux redémarrage provient du fait qu’inotify ne garantit pas un événement distinct par opération recherchée. Si deux événements de même type se produisent avant qu’un observateur/abonné ne soit informé du premier, un unique événement lui est rapporté. ↩︎

  38. https://www.man7.org/linux/man-pages/man1/inotifywatch.1.html ↩︎

  39. https://tim.siosm.fr/blog/2023/12/19/ssh-over-unix-socket/ ↩︎

  40. https://www.knot-resolver.cz/ ↩︎

  41. https://caddyserver.com/docs/caddyfile/directives/acme_server ↩︎

  42. https://etcd.io/ ↩︎

  43. https://openbao.org/ ↩︎

Expérimentation de Fedora CoreOS sur Proxmox

Ce billet est le premier d’une série de billets traitant de la création d’une infrastructure virtualisée à l’aide de Proxmox1 pour la partie hyperviseur, de Fedora CoreOS2 pour le système d’exploitation des machines virtuelles invitées (guests). L’infrastructure codifiée (Infrastructure as Code) est réalisée avec OpenTofu3 (Hashicorp Terraform ayant rejoint le côté obscur de la Force).

Présentation de Proxmox

Proxmox est un ensemble d’outils, voire une distribution Linux à part entière reposant sur Debian, permettant l’administration d’une infrastructure virtualisée en ligne de commande, via une interface web, ou grâce à une API. Proposant des options de stockage réparti, de réseau étendu à un groupe d’instances (cluster), de groupes de sécurité, de haute disponibilité, pour n’en citer que quelques-unes, il s’agit d’une solution qui peut remplacer aisément VMWare, au minimum sur les cas d’usage relativement simples.

Présentation de Fedora CoreOS

Fedora CoreOS est une distribution Linux orientée sur la stabilité et la sécurité. Son objectif principal est l’hébergement de conteneurs, et pour cette raison, elle est pourvue d’un socle minimaliste et durci, mis à jour automatiquement. Ces mises à jour automatiques sont aisément réversibles en cas de problème, grâce à son approche pseudo-immuable4, reposant sur rpm-ostree, et son système de mise à jour (Zincati) permet également d’orchestrer ces dernières de façon à maintenir un service hautement disponible.

Fedora CoreOS est également un système d’exploitation à part en cela qu’il est conçu pour ne pas avoir besoin d’interagir directement avec le système, que ce soit pour l’installation/configuration ou l’administration. L’idée est qu’un cas de changement de configuration, on réinstalle totalement le système. Au premier démarrage, lors de l’exécution de l’initramfs, le programme ignition5 exécute une recette qui configure le système : formatage et partitionnement des disques, ajout des utilisateurs, copies de fichiers et de services, etc..

Si cette approche peut sembler lourde de prime abord, elle présente en réalité l’avantage d’apporter la sérénité à ses administrateurs et administratrices ; en effet, il est dès lors possible de réaliser la recette de la nouvelle configuration à l’identique dans un environnement de préproduction, sans risque d’interférence avec l’existant (snowflake servers6).

Ignition et cloud-init7 sont des cousins éloignés. Les deux participent à la configuration et la personnalisation d’une image système. Le moment de l’exécution est cependant très différent et importe. En effet, Ignition intervient lors de l’initramfs, tandis que cloud-init intervient plus tard lors du démarrage du système d’exploitation. Ignition est donc en mesure de modifier les tables des partitions avant que le véritable système d’exploitation ne démarre, et préconfigure le système comme si cette configuration avait toujours existé ou avait été modifiée lors d’une exécution précédente.

Ignition possède néanmoins quelques limitations, assumées, notamment si on le compare à Ansible. En effet, Ansible possède de très nombreux greffons lui permettant d’effectuer des actions riches sur un système en cours d’exécution. Ignition permet essentiellement la copie de fichiers et de services, et les opérations complexes doivent être effectuées par des scripts shell (Fedora CoreOS n’installe pas Python, par exemple). Ce choix est raisonnable étant donné que relativement peu d’opérations d’administration sont attendues sur le socle faisant tourner Fedora CoreOS.

En outre, Ansible Vault permet le stockage et le déploiement sécurisé de secrets, y compris dans les outils de versionnement de code comme git. Ignition, pour sa part, ne permet pas la communication de secrets de manière sécurisée ; au mieux, il est possible de les stocker dans un fichier qui sera téléchargé puis fusionné avec le reste de la configuration par Ignition. Cette solution n’étant pas très satisfaisante, il est donc nécessaire d’utiliser un gestionnaire de secrets comme OpenBao (Hashicorp Vault ayant rejoint le côté obscur de la Force)8 avec l’emballage de réponses9, Bitwarden Send10 ou Bitwarden Secret Manager11.

Défis d’un déploiement de Fedora CoreOS sur Proxmox

Proxmox dispose d’une couche de compatibilité native avec Cloud-init. Celle-ci n’est hélas pas générique, et ne permet pas de configurer toutes les options de Cloud-init. Avec OpenTofu et le fournisseur bpg/proxmox12, il est possible de fabriquer à la volée un ISO disposant du label CIDATA, et ainsi d’exploiter le mode “nocloud” de cloud-init13. Tout ceci n’aide cependant pas au déploiement de Fedora CoreOS.

Il existe une multitude de moyens de fournir un fichier de configuration à Ignition : par HTTP après une indication par PXE ou sur la ligne de commande du noyau, directement dans le fichier ISO d’installation après une personnalisation de ce dernier, dans une variable du micrologiciel (firmware), etc. Hélas, l’utilisation d’un ISO séparé, à l’instar du mode “nocloud” de cloud-init, n’est pas une option. Nous allons néanmoins voir qu’aucune des autres options n’est native à Proxmox ou satisfaisante.

Fourniture de l’adresse du fichier Ignition par PXE

Proxmox dispose d’une couche réseau programmable/configurable (Software-defined Network (SDN)) assez développée. Cette dernière permet la définition de différentes zones réseau, avec un filtrage des flux par règles et par groupes de sécurité. En outre, elle fournit différents services, dont un serveur DHCP branché sur un IPAM (IP Address Management), avec possible synchronisation des adresses avec un serveur DNS.

Hélas, il n’est pas possible (par l’interface web ou l’API) de personnaliser la configuration DHCP pour y rajouter les options nécessaires à PXE, ni d’ajouter un service pour publier les fichiers de configuration Ignition par HTTP ou TFTP.

Il est bien sûr possible de se connecter en console, en root, pour modifier cette configuration, mais cela demande des privilèges élevés sur l’hyperviseur, et ce n’est pas officiellement supporté par Proxmox. Il est donc préférable de rechercher une voie alternative, supportée et ne nécessitant que peu de privilèges.

Fourniture du fichier de configuration par personnalisation du fichier ISO

La personnalisation du fichier ISO de Fedora CoreOS est assez aisée. En effet, ses développeurs fournissent, notamment sous la forme de conteneurs, des outils pour ajouter des fichiers et modifier la ligne de commande du noyau14.

Cette approche nécessite cependant d’avoir un fichier ISO distinct par poste à installer. Ce fichier doit à nouveau être téléversé en intégralité à chaque modification de la configuration Ignition, ce qui est consommateur de bande passante. Il peut surement être possible d’utiliser un LXC hébergé par Proxmox pour faire cette personnalisation directement sur l’hyperviseur, sans nécessiter le téléversement.

Au niveau stockage, il est possible de ne pas trop surconsommer du fait que Proxmox prend en charge nativement btrfs15 et ZFS, et que ces systèmes de fichiers permettent la déduplication des blocs. Cette approche relève cependant d’une certaine forme de bricolage, avec de potentielles pertes de performances dans le cas de ZFS ou d’espace disque dans le cas de btrfs si la déduplication n’est pas demandée assez souvent.

Fourniture du fichier de configuration par une variable du micrologiciel

La fourniture du fichier de configuration par une variable de micrologiciel s’effectue en ajoutant un argument à la commande qemu qui lance la machine virtuelle. Cet argument ressemble à la ligne suivante :

-fw_cfg name=opt/com.coreos/config,file=/local/path/to/config.ign

Cet argument peut être fourni en modifiant le fichier de configuration de la machine virtuelle sur l’hyperviseur dans le répertoire /etc/pve/qemu-server/<vm_id>.conf. Cette méthode nécessite néanmoins la connexion en utilisateur root (via le console ou en SSH), ce qui n’est pas forcément souhaitable, et la manipulation plus ou moins manuelle de fichiers système.

Il peut être également fourni lors des appels à l’API, et le greffon OpenTofu bpg/proxmox permet de le faire avec l’attribut kvm_paramters de la ressource proxmox_virtual_environment_vm. Cette méthode nécessite cependant qu’OpenTofu se connecte avec l’utilisateur root@pam, avec mot de passe et sans avoir défini de second facteur d’authentification. En effet, Proxmox ne permet pas l’utilisation de jetons d’API, même pour l’utilisateur root@pam, pour cette opération. Cette approche abaisse donc significativement le niveau de sécurité et n’est donc pas souhaitable.

En outre, l’option permettant la définition d’une variable dans le micrologiciel prend en argument le chemin vers le fichier de configuration Ignition. Ce chemin est local à l’hyperviseur. Hélas, il n’est pas possible de téléverser sur un serveur Proxmox un fichier arbitraire par l’API ou l’interface web, malgré l’existence d’une fonctionnalité de “snippets”16. Il est donc là encore nécessaire de se connecter en SSH/SFTP pour téléverser le fichier de configuration…

Convertir un fichier cloud-init

Geco IT17 a créé un script qui permet de convertir certaines options de cloud-init en fichier de configuration Ignition.

Leur méthode de création de VM ne passe pas par l’API Proxmox mais utilise un autre script shell qui lance des outils en ligne de commandes de la distribution Proxmox. Ces commandes sont exécutées en tant que root, et parmi celles-ci, la commande suivante :

qm set <vmid> -hookscript <snippet_storage>:snippets/hook-fcos.sh

L’attribut -hookscript peut être défini par l’API de Proxmox, et par le greffon bpg/proxmox d’OpenTofu. Cependant, à l’instar de l’attribut -args, celui-ci n’est accepté que si le compte d’API utilisé est root@pam, c’est-à-dire le compte root du serveur Proxmox, avec une authentification par mot de passe et sans second facteur d’authentification.

En conséquence, cette méthode n’apporte aucune sécurité additionnelle par rapport à la précédente discutée, tout en réduisant l’expressivité du fichier de configuration Ignition.

Une proposition de solution : une machine virtuelle d’installation

Comme nous avons pu le voir dans les sections précédentes, l’usage de l’API Proxmox pour les attributs -args et -hookscript est rédhibitoire puisque nécessitant non seulement d’utiliser le mot de passe du compte root dans la configuration OpenTofu, mais aussi de désactiver les seconds facteurs d’authentification.

La solution PXE est certainement la plus élégante, mais l’impossibilité de configurer le serveur DHCP de Proxmox (sans passer par le shell) pour permettre un tel démarrage nous en prive. Finalement, l’approche ISO personnalisé ne passerait pas à l’échelle d’un parc complet.

Nous disposons cependant ici de toutes les briques suffisantes pour construire une solution satisfaisante !

L’idée est la suivante : personnaliser un ISO de Fedora CoreOS afin d’y ajouter un fichier Ignition permettant d’installer un serveur DHCP (pour distribuer les IPs mais aussi la configuration PXE), un serveur HTTP pour servir les fichiers indiqués par PXE, et un serveur SFTP afin de déposer les fichiers de configuration Ignition servis par HTTP.

À l’origine, je souhaitais détailler ici la solution, mais son développement s’est avéré suffisamment complexe (et surprenant) pour constituer un billet de blog séparé. À suivre !

Identité et méthodes d'authentification

Définition des termes

Un acteur ou sujet (subject) est un membre actif d’un système d’information exerçant une action ou une activité sur les membres de ce système. Les acteurs peuvent être des personnes ou des processus.

Par opposition, un objet est un membre inactif du système d’information sur lequel peut porter une activité exercée par un acteur.

L’identité est la manière dont un acteur d’un système d’information se fait connaitre auprès des autres acteurs de ce système. On rattache à l’identité de nombreuses propriétés ou attributs. Par exemple, on peut citer l’adresse email, le numéro de téléphone, son nom et son prénom, un identifiant d’utilisateur ou d’utilisatrice (User Identifier (UID)), un identifiant de processus (Process Identifier PID), l’appartenance à des groupes, l’attribution de rôles, mais il peut s’agir plus largement de valeurs arbitraires propres à cet acteur.

L’authentification est le procédé par lequel un acteur prouve son identité aux autres acteurs du système. Il existe de nombreuses façons de prouver son identité : mots de passe, clés cryptographiques, biométrie, etc. Dans les textes juridiques, les authentifiants sont parfois appelés “moyens d’identification électroniques”.

L’autorisation encadre les activités que l’acteur peut entreprendre sur les objets du système d’information.

Identité

Portée

La portée d’une identité peut être vue comme son périmètre d’action, c’est-à-dire l’espace dans lequel cette identité a du sens (un peu comme une carte d’identité française n’a pas de valeur au Royaume-Uni). Nous allons en envisager plusieurs : locale, centrale, répartie et décentralisée.

De la portée locale

Une identité peut être locale à un système d’information, ou même à un sous-ensemble d’un système d’information. C’est le cas des comptes locaux gérés par nsswitch et le module PAM (Pluggable Authentication Modules)1 pam_unix sur une machine Linux. Les identités locales présentent l’avantage d’être autonomes et toujours disponibles, sans être tributaires de la disponibilité de système tiers. La contrepartie est une difficulté notable à maintenir la cohérence des identités sur les différents systèmes, chacun ayant la même force de vérité dans leur périmètre.

---
title: Illustration des comptes locaux
---
flowchart LR
  laptop["Client"]
  subgraph srv1 ["Serveur 1"]
    direction LR
    proc1[/"Processus 1"/]
    db1[("Base locale 1")]
  end
  subgraph srv2 ["Serveur 2"]
    direction LR
    proc2[/"Processus 2"/]
    db2[("Base locale 2")]
  end
  subgraph srv3 ["Serveur 3"]
    direction LR
    proc3[/"Processus 3"/]
    db3[("Base locale 3")]
  end

  laptop --> srv1 & srv2 & srv3
  proc1 --> db1
  proc2 --> db2
  proc3 --> db3

Des référentiels d’identités centraux

La portée d’une identité peut être plus large et une identité peut être connue de plusieurs membres d’un système d’information ; on parle alors d’une identité centralisée.

Un référentiel d’identités central contient les identités d’un ensemble d’acteurs.

L’intérêt d’un tel référentiel devient évident à mesure qu’un système d’information s’agrandit ; gérer manuellement des identités locales sur de nombreux systèmes peut mener à des incohérences, et des oublis. En rassemblant les informations dans un référentiel central, il n’y alors qu’une seule source de vérité, faisant autorité pour l’ensemble du système d’information concerné. Il convient également de noter qu’il peut exister des copies de ces informations conservées en local sur les différents systèmes assujettis à ce référentiel d’identité. Ces copies ne font pas autorité, et il convient de surveiller leur cohérence avec la source de vérité et de les invalider ou les mettre à jour, le cas échéant.

Les copies sont souvent nécessaires à des fins de performance et de résilience. En effet, centraliser les identités présente des défis techniques notamment en matière de disponibilité ; une panne du référentiel central peut être de nature à stopper net l’ensemble des activités d’un système d’information.

Parmi les protocoles fréquemment utilisés pour la centralisation des identités, il est possible de citer LDAP2.

---
title: Illustration d'un annuaire centralisé
---
flowchart LR
  laptop["Client"]
  subgraph srv1 ["Serveur 1"]
    direction TB
    proc1[/"Processus 1"/]
    dbs1[("Copie locale")]
  end
  subgraph srv2 ["Serveur 2"]
    direction TB
    proc2[/"Processus 2"/]
    dbs2[("Copie locale")]
  end
  subgraph srv3 ["Serveur 3"]
    direction TB
    proc3[/"Processus 3"/]
    dbs3[("Copie locale")]
  end

  subgraph ref ["Référentiel d'identité"]
    ldapsrv[/"Serveur LDAP"/]
    db[("Base d'identités")]
  end

  laptop --> srv1 & srv2 & srv3
  srv1 & srv2 & srv3 --"LDAP"--> ref
  proc1 --> dbs1
  proc2--> dbs2
  proc3 --> dbs3
  ldapsrv --> db

Des référentiels d’identités répartis

Un référentiel d’identités peut être amené à voir sa taille grandir, que ce soit de manière organique ou lors d’acquisitions de sociétés. Dans ce dernier cas, en particulier, il existe alors plusieurs référentiels d’identités concurrents, qu’il pourrait être intéressant de fusionner. Cette opération est sensible, et difficile.

Il existe des alternatives à la fusion ; l’établissement d’une forêt3 ou la mise en place d’un annuaire virtuel (Virtual Directory Service (VDS))4.

Une forêt est constituée d’un ensemble de référentiels d’identités indépendants entre lesquels il existe une relation de confiance. Ainsi, par transitivité de la confiance, les membres d’un référentiel d’identité accordent leur confiance aux identités gérées par un autre référentiel d’identité. Ce genre de schéma est très répandu dans les infrastructures Active Directory.

Les services d’annuaires virtuels jouent le rôle de serveurs mandataires pour l’accès à plusieurs référentiels d’identité. Ces derniers apparaissent alors comme “fusionnés” aux yeux des utilisateurs de service, alors qu’ils continuent d’être gérés individuellement.

---
title: Illustration d'annuaires répartis (forest)
---
flowchart LR
  laptop["Client"]
  subgraph srv1 ["Serveur 1"]
    direction TB
    proc1[/"Processus 1"/]
    dbs1[("Copie locale")]
  end
  subgraph srv2 ["Serveur 2"]
    direction TB
    proc2[/"Processus 2"/]
    dbs2[("Copie locale")]
  end

  subgraph refnet ["Référentiel d'identité #quot;example.net#quot;"]
    direction TB
    ldapsrv[/"Serveur LDAP"/]
    db[("Base d'identités")]
  end

  subgraph refcom ["Référentiel d'identité #quot;example.com#quot;"]
    direction TB
    ldapsrv2[/"Serveur LDAP"/]
    db2[("Base d'identités")]
  end

  laptop --> srv1 & srv2
  srv1 & srv2 --"LDAP"--> refcom
  proc1 --> dbs1
  proc2 --> dbs2
  ldapsrv --> db
  srv1 & srv2 --"LDAP"--> refnet
  ldapsrv2 --> db2
---
title: Illustration d'annuaires répartis (VDS)
---
flowchart LR
  laptop["Client"]
  subgraph srv1 ["Serveur 1"]
    proc1[/"Processus 1"/]
    dbs1[("Copie locale")]
  end
  subgraph srv2 ["Serveur 2"]
    proc2[/"Processus 2"/]
    dbs2[("Copie locale")]
  end
  subgraph srv3 ["Serveur 3"]
    proc3[/"Processus 3"/]
    dbs3[("Copie locale")]
  end

  subgraph vds ["Virtual Directory Service"]
    direction TB
    vdss[/"Serveur VDS"/]
    dbss[("Copie locale")]
  end

  subgraph refnet ["Référentiel d'identité #quot;example.net#quot;"]
    ldapsrv[/"Serveur LDAP"/]
    db[("Base d'identités")]
  end

  subgraph refcom ["Référentiel d'identité #quot;example.com#quot;"]
    ldapsrv2[/"Serveur LDAP"/]
    db2[("Base d'identités")]
  end

  laptop --> srv1 & srv2 & srv3
  proc1 --> dbs1
  proc2 --> dbs2
  proc3 --> dbs3
  srv1 & srv2 & srv3 --"LDAP"--> vds
  vds --"LDAP"--> refcom & refnet
  vdss --> dbss
  ldapsrv --> db
  ldapsrv2 --> db2

Des référentiels d’identités décentralisés

Les référentiels d’identités répartis ont du sens lorsque l’ensemble des référentiels d’identités sont sous le contrôle d’entités juridiques ayant un rapport de confiance ou ayant la volonté d’apparaitre sous une même bannière. La confiance et l’unification s’effectuent par les gestionnaires des référentiels d’identités ; le consommateur de ces référentiels n’a aucun pouvoir de décision.

Il existe cependant une alternative dans le cas où ce genre de centralisation ou de répartition n’est pas possible ou souhaitable : l’utilisation de référentiels d’identités décentralisés. On parle alors d’identités fédérées.

Avec des référentiels d’identités décentralisés, chaque référentiel est géré par ses gestionnaires selon des politiques qui leur sont propres. C’est, en revanche, les responsables des systèmes d’information ayant besoin de connaitre l’identité de leurs utilisateurs et utilisatrices qui choisissent à quels fournisseurs de référentiels d’identités décentralisés ils font confiance ! Si l’identité d’un utilisateur ou d’une utilisatrice n’est pas gérée par un fournisseur de référentiels d’identité accrédité par les responsables d’un système d’information, il ou elle ne pourra pas se faire connaitre de ce système d’information.

Concernant la disponibilité, la plupart des services consommant des identités fédérées maintiennent en local une base de données avec des attributs supplémentaires spécifiques à leurs besoins métier. En revanche, si un fournisseur de référentiel d’identités est indisponible, les utilisateurs et les utilisatrices de ce fournisseur ne pourront plus se connecter à aucun service.

Les référentiels d’identités décentralisés sont devenus très populaires sur Internet, et de nombreux sites proposent ainsi de s’enregistrer ou de s’identifier à l’aide d’une identité fédérée. Ironiquement, cependant, peu de référentiels d’identités sont accrédités par les sites web, et les identités décentralisées reconnues se limitent souvent à celles gérées par quelques acteurs clés : Google, Facebook, Twitter, ou spécifiquement en France, France Connect.

L’interrogation de ces référentiels se fait à l’aide de protocoles plus ou moins standards. Parmi les plus répandus, on peut notamment citer OpenID Connect5 et SAML6.

---
title: Illustration d'annuaires décentralisés
---
flowchart LR
  laptop["Client"]
  subgraph srv [Serveur]
    direction TB
    proc[/"Processus"/]
    dbs[("Base locale")]
  end

  subgraph refnet ["Référentiel d'identité #quot;example.net#quot;"]
    direction TB
    oidcsrv1[/"Serveur OpenID Connect"/]
    db1[("Base d'identités")]
  end

  subgraph refcom ["Référentiel d'identité #quot;example.com#quot;"]
    direction TB
    samlsrv1[/"Serveur SAML"/]
    db2[("Base d'identités")]
  end

  subgraph reforg ["Référentiel d'identité #quot;example.org#quot;"]
    direction TB
    oidcsrv2[/"Serveur OpenID Connect"/]
    db3[("Base d'identités")]
  end


  laptop --"Protocole OIDC/SAML"--> srv
  srv --"Accrédite"--> refnet
  srv --"Accrédite"--> refcom
  srv --"`N'accrédite *PAS*`"--> reforg;
  proc --> dbs
  laptop <-."Protocole OIDC".-> refnet
  laptop <-."Protocole SAML".-> refcom
  oidcsrv1 --> db1
  samlsrv1 --> db2
  oidcsrv2 --> db3
  linkStyle 1,2 color:red;

Sur la séparation des référentiels d’identité

Le référentiel SecNumCloud78 prévoit que les comptes d’administration d’un prestataire de cloud qualifié soient gérés “à l’aide d’outils et d’annuaires distincts de ceux utilisés pour la gestion des comptes utilisateurs placés sous la responsabilité du commanditaire (c’est-à-dire de ses clients)”.

Exigée pour les prestataires de cloud qualifiés, cette séparation des référentiels d’identités afin d’en dédier un pour les comptes d’administration du système d’information semble une bonne pratique de sécurité à adopter en général. Elle permet, en effet, de limiter la surface d’attaque des services manipulant notamment les clés cryptographiques utilisées pour l’administration (clés TLS, clés de signature des “jetons d’accès”, etc.).

Authentification

L’authentification est un sujet complexe, que ce cours va aborder sous de nombreux angles :

  • les aspects juridiques et normatifs ;
  • les facteurs d’authentification ;
  • la force de l’authentification ;
  • les mécanismes de preuve mis en œuvre ;
  • les risques de la réutilisation de moyens de preuve d’identité ;
  • le stockage des éléments authentifiants.

Aspects juridiques de l’authentification

L’authentification est un sujet traité à la fois par les juridictions nationales et internationales. Un défaut de conformité à ces dispositions peut entrainer des sanctions parfois sévères.

La Commission Nationale Information et Libertés (CNIL) a à plusieurs reprises sanctionné des sociétés mettant en place des mécanismes d’authentification trop faibles9, des politiques de sécurité entourant les moyens d’authentifications insuffisantes10, ou des mécanismes de stockage des éléments authentifiants trop faibles, mal déployés ou mal composés11.

Parmi les textes juridiques encadrant l’authentification, on peut notamment évoquer le règlement européen “Electronic Identification, Authentication and Trust Services” (eiDAS)12 et la “directive révisée sur les services de paiement” (DSP2)13.

En France, la directive DSP2 a été transposée notamment dans les articles L133-414 et L133-4415 du code monétaire et financier.

Pour le règlement eiDAS, des spécifications techniques viennent compléter les textes juridiques, notamment à des fins d’interopérabilité16. Des précisions techniques sont également apportées sur les contraintes pour satisfaire les différents niveaux de garantie définis par le règlement17. Une ordonnance18 donne le pouvoir à l’Agence Nationale de la Sécurité des Systèmes d’Information (ANSSI)19 de certifier des moyens d’authentification permettant de satisfaire ces niveaux de garantie. De plus amples informations sur le règlement eiDAS peuvent être trouvées sur le site de l’ANSSI20.

Nous reviendrons sur le contenu de ces textes au fur et à mesure que le cours abordera les notions qui y sont renseignées.

Outre les textes à valeurs juridiques, en France, l’ANSSI a publié divers référentiels et guides :

  • le Référentiel Général de Sécurité (RGS) 2.0, et en particulier son annexe B1 sur les mécanismes cryptographiques et son annexe B3 sur les mécanismes d’authentification21 ;
  • le guide “Recommandations relatives à l’authentification multifacteur et aux mots de passe”[^guideauthn].

Le respect des règles et “recommandations” listées dans ces documents est requis pour l’obtention d’un certain nombre de qualifications de l’ANSSI, notamment SecNumCloud7.

Les facteurs d’authentification

Le règlement d’exécution (UE) 2015/150217 définit trois types de facteurs d’authentification.

a) “facteur d’authentification basé sur la possession”, un facteur d’authentification dont il revient au sujet de démontrer la possession;

b) “facteur d’authentification basé sur la connaissance”, un facteur d’authentification dont il revient au sujet de démontrer la connaissance;

c) “facteur d’authentification inhérent”, un facteur d’authentification qui est basé sur un attribut physique d’une personne physique, et dont il revient au sujet de démontrer qu’il possède cet attribut physique.

Cette définition est également présente dans le code monétaire et financier, ainsi que dans le RGS (à une subtilité prête ; voir le chapitre concernant la force de l’authentification).

Les facteurs d’authentification basés sur la possession

D’après le guide de l’ANSSI “Recommandations relatives à l’authentification multifacteur et aux mots de passe” :

Un facteur de possession est un moyen de stocker des secrets non mémorisables par un humain (et n’ayant pas vocation à être mémorisés par un humain). Il s’agit typiquement des clés cryptographiques qui permettent de réaliser des opérations de chiffrement, de signature ou d’authentification.

Le même guide spécifie également :

Un facteur de possession doit être un équipement attribué à un unique utilisateur. Afin de garantir la sécurité apportée par ce facteur, il est essentiel que des moyens de protection et de détection contre les tentatives de reproduction ou de falsification du facteur soient mis en place. Un facteur de possession peut être une carte à puce contenant une clé privée, une carte SIM d’un téléphone mobile comportant des données d’identification, ou un dispositif permettant de générer des codes à usage unique (OTP).

Les facteurs d’authentification basés sur la connaissance

Les facteurs d’authentification basés sur la connaissance reposent sur le fait que l’acteur connaisse un secret. En général, il s’agit d’un mot de passe, d’une phrase de passe, ou d’un code PIN (Personal Identification Number).

Les jetons d’authentification (authentication token ou bearer token) sont un cas intéressant lorsqu’on parle d’authentification multifacteurs. En effet, ces derniers sont soit des valeurs aléatoires assimilables à des clés cryptographiques symétriques, soit des informations structurées et signées et encodées sous la forme d’une chaine de caractères (par exemple en base64). La simple preuve de la connaissance d’un tel jeton est suffisante pour prouver son identité ; cela laisserait donc entendre qu’il s’agisse d’un facteur d’authentification basé sur la connaissance. Pourtant, il s’agit d’un secret non mémorisable par un humain, ce qui entrerait aussi dans certaines des définitions d’un facteur d’authentification basé sur la possession.

Les facteurs d’authentification inhérents

L’authentification par des facteurs inhérents est une question épineuse en France. En effet, l’ANSSI a longtemps été une pourfendeuse de la biométrie, indiquant que les facteurs inhérents sont au mieux un facteur d’identification et non d’authentification, ou dans le pire des cas une méthode de déverrouillage pour un autre facteur d’authentification. Cette position a dû être partiellement révisée par la contrainte européenne, au travers du règlement eiDAS et la directive DSP2.

Cette méfiance vis-à-vis de la biométrie n’est pourtant pas sans justification.

La vérification des facteurs inhérents s’effectue de manière probabiliste ; contrairement à la vérification des mots de passe ou des signatures cryptographiques, l’acquisition des données nécessaires à ce type de vérification n’est pas “parfaite”, et les algorithmes sont paramétrés avec des taux de faux positifs (validation d’une personne non autorisée) et de faux négatifs (refus d’une personne autorisée). Suivant le paramétrage, l’utilisateur fait alors face soit à un risque en confidentialité et en intégrité, soit à un risque en disponibilité. Ces paramétrages doivent d’ailleurs tenir compte qu’au cours du temps, certaines caractéristiques physiques changent ou s’estompent.

Ensuite, les facteurs inhérents ne sont pas renouvelables à l’infini ; si la révocation d’un moyen est possible, son remplacement ne l’est pas forcément.

Également, le risque de réutilisation d’une même caractéristique physique pour l’authentification auprès de plusieurs systèmes d’information peut comporter des risques de compromission croisée : un système d’information compromis pourrait permettre d’en compromettre un autre, à la manière d’une attaque dite de “credential stuffing22. Cette attaque ne se limite cependant pas aux exfiltrations de données biométriques depuis des bases de données compromises ; par le passé, des chercheurs en sécurité ont réussi à extraire des données biométriques à partir de simples photos23.

Remarques concernant l’authentification des processus

La notion de facteurs d’authentification n’a réellement de sens que pour les utilisateurs. Par définition, un processus ne peut posséder un équipement ou mettre en œuvre un attribut physique.

Pourtant, l’exigence 9.6, alinéa f du référentiel SecNumCloud dispose que :

Le prestataire doit mettre en place un système d’authentification multifacteur fort pour l’accès :

  • aux interfaces d’administration utilisées par le prestataire ;
  • aux interfaces d’administration dédiées aux commanditaires.

Avec une lecture littérale, il semblerait donc impossible d’authentifier des processus autonomes (par exemple, les tâches planifiées ou les traitements de chaines d’intégration ou de déploiement continu (CI/CD)) pour l’administration d’un cloud qualifié SecNumCloud. Fort heureusement, les auditeurs SecNumCloud ne suivent pas à la lettre cette exigence mal écrite, et les prestataires de Cloud qualifiés proposent pour la plupart des interfaces d’administration programmables (API) sans avoir recours à une authentification multifacteur.

Il convient cependant de noter que strictement parlant, il pourrait être considéré qu’un processus puisse mettre en œuvre un facteur d’authentification basé sur la possession à l’aide d’équipements de sécurité particuliers : les Trusted Platform Module (TPM)24, les Hardware Security Module (HSM)25, et les Key Management Services (KMS)26. Ces équipements permettent la manipulation ou l’exercice de secrets sans en permettre l’extraction, la copie ou la falsification.

La force de l’authentification

Le règlement eiDAS définit trois niveaux de garantie : faible, substantiel et élevé.

À chacun de ces niveaux de garantie sont associées des contraintes particulières. Parmi ces dernières, on peut notamment citer le nombre de facteurs d’authentification requis. Ainsi, le niveau faible ne requiert l’usage que d’un seul facteur, tandis que le niveau substantiel requiert au moins deux facteurs de nature différente.

La directive européenne DSP2 transposée dans le code monétaire et financier définit l’authentification forte comme étant :

[…] une authentification reposant sur l’utilisation de deux éléments ou plus appartenant aux catégories “connaissance” (quelque chose que seul l’utilisateur connait), “possession” (quelque chose que seul l’utilisateur possède) et “inhérence” (quelque chose que l’utilisateur est) et indépendants en ce sens que la compromission de l’un ne remet pas en question la fiabilité des autres, et qui est conçue de manière à protéger la confidentialité des données d’authentification ;

Cette définition de l’article L133-44 du code monétaire et financier vient en contradiction avec la définition de l’authentification forte que l’ANSSI emploie parfois. En effet, la définition de l’authentification forte dans le RGS est (presque) conforme à la définition du droit français:

Cette recommandation est en cohérence avec la notion traditionnelle « d’authentification forte », qui préconise de combiner deux mécanismes parmi ce que l’on sait, ce que l’on a, ce que l’on est ou ce que l’on sait faire.

On notera au passage que le RGS évoque “ce que l’on sait faire” qui est un facteur qui n’est ni reconnu par le règlement eiDAS, ni le code monétaire et financier.

En revanche, dans son guide “Recommandations relatives à l’authentification multifacteur et aux mots de passe” en version 2.0, le chapitre 2.5 distingue “authentification forte” et “authentification multifacteur”, en précisant :

En langue française, l’authentification multifacteur est souvent confondue avec l’appellation authentification forte (ou robuste), ce qui laisserait entendre qu’une authentification multifacteur est nécessairement plus robuste qu’une authentification avec un unique facteur.

[…]

Afin d’être considérée comme forte, une authentification doit reposer sur un protocole cryptographique permettant de résister à certaines attaques comme :

  • l’écoute clandestine (eavesdroppping en anglais), qui consiste pour un attaquant à passivement écouter le canal de communication entre le prouveur et le vérifieur ;
  • les attaques par rejeu, qui consistent pour un attaquant à récupérer des informations d’authentification (comme un mot de passe ou son empreinte) et à utiliser ces informations pour les rejouer afin d’usurper l’identité de la cible (l’attaque pass-the-hash [21] en est un exemple) ;
  • les attaques de l’homme-du-milieu, qui consistent pour un attaquant à intercepter et modifier les communications se déroulant entre le prouveur et le vérifieur lors de l’authentification sans être détecté ;
  • la non-forgeabilité : l’observation par un attaquant de plusieurs échanges d’authentification d’un prouveur ne doit pas lui permettre d’usurper son identité dans un nouvel échange d’authentification.

En outre, l’expression “authentification forte” est de plus en plus galvaudée par le marketing.

Il convient donc de toujours s’assurer de la définition de l’authentification forte qu’un interlocuteur ou une interlocutrice utilise.

Les mécanismes d’authentification

Cette section du cours n’a pas vocation à être exhaustive. La liste qui suit n’est même pas forcément représentative des mécanismes les plus fréquemment utilisés. Elle permet cependant de couvrir un large éventail de techniques afin d’apporter une culture générale sur le sujet.

Dans cette section, on utilisera le terme de prouveur pour désigner celui qui tente de prouver son identité ; c’est-à-dire de s’authentifier. Le terme vérificateur désigne celui qui vérifie les preuves d’identité. On peut penser de prime abord que seul l’utilisateur tente de prouver son identité auprès d’un serveur pour accéder à son compte. La réalité est que presque toujours, le serveur prouve également son identité auprès du logiciel mis en œuvre par l’utilisateur ou l’utilisatrice. En conséquence, il serait incorrect de considérer que prouveur et utilisateurs sont synonymes dans le chapitre qui suit.

La preuve par divulgation

La méthode la plus universelle pour prouver son identité est de divulguer un secret au vérificateur. Ce dernier peut alors comparer ce secret à celui attendu pour cet acteur. S’ils correspondent, alors la preuve est faite. C’est le cas typique de l’utilisateur qui envoie son mot de passe tel quel à un serveur. C’est également le cas pour les codes temporels (TOTP), les codes Transaction Authentification Number (TAN) (par exemple, des codes reçus par email ou par SMS et à reproduire auprès du vérificateur), les codes PIN, les jetons d’authentification, etc. C’est également le cas des “identifiants de session”, comme les “cookies de session”, qui ne sont ni plus ni moins que des jetons d’authentification présentés à chacune des requêtes.

Ce mécanisme d’authentification présente de nombreux défauts, et a pour seul avantage d’être extrêmement simple à implémenter, à tout le moins du côté du prouveur. Côté vérificateur, les choses sont plus complexe : il s’agit de stocker de manière sécurisée le matériel nécessaire à la vérification de l’identité. Ce stockage sécurisé est l’objet d’un chapitre ultérieur.

Outre la nécessité d’un stockage sécurisé, ce mécanisme d’authentification nécessite que le secret exige sur un canal de communication assurant la confidentialité des données. Sans cela, un attaquant ou une attaquante n’aurait qu’à écouter passivement le réseau pour apprendre le secret. En outre, tous les logiciels manipulant le secret en clair et dans sa forme originale, tel que connu du prouveur, devraient mettre en place des mesures de sécurité afin de limiter les risques de divulgation de ce secret en cas de compromission logicielle : nettoyer la mémoire, mettre en place des pages de garde (page guards), isoler les traitements des secrets dans des processus séparés. En pratique, cela n’est presque jamais fait. C’est ainsi que des vulnérabilités comme Heartbleed27, qui avait permis de faire des lectures arbitraires dans la mémoire des processus utilisant OpenSSL pour sécuriser les communications, ont permis de récupérer les mots de passe non protégés directement en mémoire. Tous les secrets ainsi exfiltrés étaient directement utilisables pour se connecter aux comptes associés.

Finalement, ce mécanisme d’authentification ne présente aucune protection contre le rejeu. Ainsi, si le canal de communication est intercepté ou si l’utilisateur ou l’utilisatrice est victime d’une attaque par hameçonnage (phishing), et qu’il ou elle saisit son mot de passe sur le site frauduleux, alors l’attaquant ou l’attaquante apprendra le moyen de preuve et pourra le mettre en œuvre pour accéder au compte associé. Il conviendra néanmoins de nuancer l’impact de la vulnérabilité aux attaques par rejeu avec la durée de vie du secret divulgué : certains sont à usage unique ou ont une faible durée de validité (TOTP, TAN, etc.), tandis que d’autres ont des durées de vie beaucoup plus longues (par exemple, les mots de passe).

---
title: Authentification par divulgation d'un mot de passe
---
sequenceDiagram
  autonumber
  actor peggy as Peggy
  box
    actor victor as Victor
    participant db as Base de données
  end

  peggy ->> victor:"Je m'appelle Peggy. Mon mot de passe est #quot;Bonjour1!#quot;."

  victor ->> victor:Calcule la dérivée du mot de passe reçu

  victor ->> db:"Quel est la dérivée du mot de pase de Peggy ?"
  db ->> victor:"Voici la dérivée du mot de passe de Peggy"

  victor ->> victor:Compare les deux dérivées

  alt Mot de passe valide
    victor ->> peggy:"Bonjour Peggy"
  else Mot de passe invalide
    victor ->> peggy:"Je ne crois pas, non"
  end

La preuve par défi

Les mécanismes d’authentification reposant sur des défis sont des procédés interactifs : un dialogue se met en place entre le vérificateur et le prouveur. Ce dialogue permet de s’accorder sur une valeur aléatoire, parfois appelée nonce. Cet accord peut être unilatéral : le vérificateur décide arbitrairement de cette valeur. Ensuite le prouveur effectue une opération cryptographique sur cette valeur aléatoire et envoie le résultat au vérificateur. Ce dernier vérifie alors la preuve à l’aide d’un élément en sa possession et qui est associé à l’identité supposée du prouveur.

Il existe de nombreux protocoles reposant sur les défis ; certains ont de très bonnes propriétés de sécurité ; d’autres sont catastrophiquement mauvais, et même pires que la preuve par divulgation !

RFC 7616 : HTTP Digest

La RFC 761628 décrit le mécanisme d’authentification HTTP Digest.

Le vérificateur génère un nonce et l’envoie au prouveur. Ce dernier utilise alors une fonction de hachage cryptographique sur la concaténation de l’identité à prouver, le mot de passe associé à cette identité, le nonce, le verbe HTTP et l’URI demandée. Le résultat de la fonction de hachage est envoyé au vérificateur. Ce dernier exécute la fonction de hachage de son côté avec les mêmes entrées. Si le résultat de la fonction de hachage appelée par le vérificateur est identique à celui transmis par le prouveur, alors la preuve est faite.

L’intérêt supposé de ce mécanisme d’authentification est que le mot de passe n’est à aucun moment transmis dans la requête HTTP. Celui-ci est protégé par la résistance de la fonction de hachage cryptographique à la découverte d’une préimage29.

Cet intérêt supposé est cependant assez peu pertinent étant donné que l’essentiel des communications HTTP est de nos jours transporté sur des canaux de communication chiffrés (par TLS ou HTTP/3 qui intègrent directement le chiffrement).

Un intérêt notable de ce mécanisme d’authentification est l’inclusion d’un nonce. Ce dernier permet de contrer les attaques par rejeu ; en effet, même en cas d’interception d’une preuve, il n’est pas possible de l’utiliser pour réaliser des tentatives d’authentification futures par simple rejeu.

En revanche, ce mécanisme d’authentification requiert que le vérificateur connaisse le mot de passe de l’utilisateur30 et qu’il le stocke tel quel, ou sous un format réversible. En conséquence, en cas de fuite de la base de données du vérificateur, un attaquant pourrait immédiatement utiliser les valeurs récupérées pour usurper l’identité de tous les acteurs auprès de ce vérificateur. Cet inconvénient majeur disqualifie totalement ce mécanisme d’authentification, qui ne doit jamais être employé.

Un autre problème de sécurité est que la divulgation de toute preuve à un attaquant ou une attaquante lui permet d’effectuer ensuite une recherche exhaustive hors-ligne sur le mot de passe qui a permis de générer cette preuve ; tous les autres éléments sont connus. La possibilité de recherche exhaustive hors-ligne sur des mots de passe est agravée par le fait que les fonctions de hachage spécifiées dans la RFC sont très insuffisantes ; ces considérations de sécurité sont identiques à celles discutées dans le chapitre sur le stockage des dérivées des mots de passe.

---
title: Authentification avec HTTP Digest (simplifiée)
---
sequenceDiagram
  autonumber
  actor peggy as Peggy
  box
    actor victor as Victor
    participant db as Base de données
  end

  peggy ->> victor:"Je voudrais faire X sur l'adresse Y."

  victor ->> peggy:"Heu, t'es qui ? Voici un nonce."

  peggy ->> peggy: Calcule le condensat, avec son nom, son mot de passe, X, Y et le nonce

  peggy ->> victor:"Je m'appelle Peggy. Je voudrais faire X sur l'adresse Y. Voici le nonce et mon condensat."

  victor ->> db: "C'est quoi le mot de passe de Peggy ?"

  db ->> victor: "Le mot de passe de Peggy est #quot;Bonjour1!#quot;..."

  victor ->> victor:Calcule le condensat, avec le nom #quot;Peggy#quot;, le mot de passe de Peggy, X, Y et le nonce

  alt Les condensats correspondent
    victor ->> peggy:"Bonjour Peggy. Voici le résultat de X sur Y."
  else Les condensats ne correspondent pas
    victor ->> peggy:"Je ne crois pas, non"
  end

La signature électronique

La signature électronique est une méthode d’authentification très courante. Elle est notamment employée par les protocoles SSH, TLS, et IPsec.

Le principe général est que le prouveur dispose d’une clé privée et le vérificateur dispose de la clé publique associée. Le prouveur effectue la signature électronique d’un nonce et le vérificateur n’a alors qu’à utiliser la clé publique pour en vérifier l’authenticité. Si elle est authentique, alors la preuve est faite.

Ce mécanisme d’authentification présente l’avantage de lever toute contrainte relative à la confidentialité du stockage par le vérificateur : seule une clé publique est stockée, dont la divulgation ne remet pas en cause la sécurité du mécanisme d’authentification. La contrainte d’intégrité du stockage de la clé publique peut elle-même être levée si la clé publique est inscrite dans un certificat, faisait partie d’une infrastructure de clés. Il faudra néanmoins assurer l’intégrité du certificat racine de l’infrastructure de clés.

Des précautions doivent cependant être prises, notamment pour éviter les attaques par rejeu. En effet, il est important que le prouveur s’assure que les nonces ne soient jamais réutilisés (en général ou au minimum par un même vérificateur). En effet, si un nonce était réutilisé, il ne serait plus nécessaire d’avoir accès à la clé privée permettant la signature ; il suffirait juste de rejouer la preuve correspondante qui aurait été générée la première fois que ce nonce avait été signé. Un attaquant ou une attaquante pourrait également hameçonner un prouveur dans le cadre d’une attaque par relais. Cette attaque consiste alors à présenter au prouveur le nonce choisi par le vérificateur, récupérer cette preuve et la jouer à son compte auprès du véritable vérificateur.

Pour contrer ces attaques par rejeu ou par relais, la plupart des implémentations font intervenir le prouveur dans le choix du nonce ; le prouveur est alors sûr qu’au moins sa propre contribution à l’établissement du nonce est réellement aléatoire et non rejouée.

Une autre contremesure aux attaques par relais est de s’assurer que le prouveur utilise une clé privée différente par vérificateur. Le prouveur peut utiliser, par exemple, le nom du site web consulté comme discriminant. Ainsi, en cas d’hameçonnage, le site web de l’attaquant ou de l’attaquante ne dispose pas de la même URL et, pour générer la signature, la clé privée utilisée est différente de la clé qui serait utilisée pour le vérificateur légitime. En conséquence, la preuve interceptée par l’attaquant ou l’attaquante n’est pas valide auprès du véritable vérificateur ! Cette parade peut être utilisée en plus de la précédente ; leur combinaison est utilisée par les protocoles WebAuthn, U2F et FIDO2, détaillés ultérieurement dans ce cours.

---
title: Authentification d'un serveur TLS avec confidentialité persistente (simplifiée)
---
sequenceDiagram
  autonumber
  actor peggy as Peggy (Serveur TLS)
  actor victor as Victor (Client TLS)

  victor ->> victor:Tire un nonce

  victor ->> victor:Tire une bi-clé temporaire (DHE)

  victor ->> peggy:"Je voudrais consulter https://broken-by-design.fr. Voici un nonce et une clé publique temporaire"

  peggy ->> peggy:Tire un nonce

  peggy ->> peggy:Tire une bi-clé temporaire (DHE)

  peggy ->> peggy:Prépare le message "Voici un autre nonce"

  peggy ->> peggy:Prépare le message "Voici un certificat pour broken-by-design.fr"

  peggy ->> peggy:Prépare le message "Voici une autre clé publique temporaire"

  peggy ->> peggy:Calcule un condensat cryptographique de l'ensemble des messages échangés et préparés

  peggy ->> peggy:Signe avec sa clé privée le condensat cryptographique

  peggy ->> victor:"Voici un autre nonce"

  peggy ->> victor:"Voici un certificat pour broken-by-design.fr"

  peggy ->> victor:"Voici une autre clé publique temporaire"

  peggy ->> victor:"Voici la signature des messages précédents"

  victor ->> victor:Vérifie l'authenticité du certificat

  victor ->> victor:Calcule le condensat cryptographique de l'ensemble des messages reçus (sauf la signature).

  victor ->> victor:Vérifie la signature cryptographique reçue avec la clé publique du certificat

  alt La signature est valide
    par Calcul des clés par Peggy
      peggy ->> victor:"Top ! À partir de maintenant, échangeons de manière sécurisée."
      peggy ->> peggy:Calcule le PMS et les autres clés
    and Calcul des clés par Victor
      victor ->> peggy:"Top ! À partir de maintenant, échangeons de manière sécurisée."
      victor ->> victor:Calcule le PMS et les autres clés
    end
    victor -> peggy:"#10216;illisible#10217; (vérification de l'intégrité des échanges précédents)"
    victor -> peggy:"#10216;illisible#10217; (échanges métier)"
  else La signature est invalide
    victor ->> peggy:"Je ne crois pas, non"
  end

La preuve par déchiffrement

La preuve par déchiffrement repose sur la capacité du prouveur à démontrer la connaissance d’un secret arbitraire qui lui a été transmis sous une forme chiffrée par le vérificateur.

Pour déchiffrer ce secret arbitraire, le prouveur doit disposer d’une clé symétrique ou d’une clé privée. La clé symétrique peut être issue d’un calcul, comme le résultat d’une fonction de dérivation de mot de passe (voir chapitre sur le stockage des éléments authentifiants pour en apprendre plus sur ces fonctions).

En déchiffrant le secret arbitraire envoyé par le vérificateur, le prouveur démontre qu’il connait la clé de déchiffrement et donc qu’il est celui qu’il prétend être. Cela est pourtant insuffisant pour convaincre le vérificateur, puisque ce dernier ne sait encore rien du résultat de ce déchiffrement. Une nouvelle opération cryptographique est donc nécessaire : le prouveur doit exercer ce secret arbitraire dans une opération cryptographique et envoyer le résultat de cette dernière au vérificateur. Le vérificateur pourra alors vérifier que l’opération cryptographique a bien mis en œuvre le secret qu’il avait envoyé au prouveur.

Les implémentations de cette preuve varient beaucoup. Elle est employée par le protocole Kerberos ou par certaines suites cryptographiques de TLS.

Avec les suites cryptographiques de TLS ne mettant pas en œuvre la confidentialité persistante (perfect forward secrecy (PFS)), le vérificateur (le client TLS) va envoyer au prouveur (le serveur TLS) un secret chiffré avec la clé publique contenue dans le certificat du serveur TLS. Si le serveur TLS est bien en possession de la clé privée associée à la clé publique contenue dans le certificat, il pourra déchiffrer ce secret. Ce secret, appelé “pre-master secret (PMS)” est utilisé ensuite par le vérificateur et le prouveur pour calculer les clés cryptographiques qui seront utilisées pour sécuriser l’ensemble de la communication TLS. Si le prouveur n’a pas été en mesure de déchiffrer le PMS, il ne sera pas en mesure de lire, ni de répondre aux messages chiffrés envoyés par le vérificateur. Si en revanche, il est en mesure de le faire, alors implicitement, il aura prouvé qu’il était en possession de la clé privée et la preuve d’identité sera faite.

---
title: Authentification TLS sans confidentialité persistente (simplifiée)
---
sequenceDiagram
  autonumber
  actor peggy as Peggy (Serveur TLS)
  actor victor as Victor (Client TLS)

  victor ->> victor:Tire un nonce

  victor ->> peggy:"Je voudrais consulter https://broken-by-design.fr. Voici un nonce."

  peggy ->> peggy:Tire un autre nonce

  peggy ->> victor:"Voici un autre nonce"

  peggy ->> victor:"Voici un certificat pour broken-by-design.fr"

  victor ->> victor:Vérifie l'authenticité du certificat reçu

  victor ->> victor:Tire le PMS

  victor ->> victor:Chiffre le PMS avec la clé publique du certificat

  victor ->> peggy:"Voici le PMS chiffré"

  par Peggy indique chiffrer ses communications
      peggy ->> victor:"Top ! À partir de maintenant, échangeons de manière sécurisée."
  and Victor indique chiffrer ses communications
      victor ->> peggy:"Top ! À partir de maintenant, échangeons de manière sécurisée."
  end

  alt Peggy ne connait pas la clé privée
    peggy --x victor:Peggy ne dispose pas des clés pour chiffrer un message pour Victor
  else Peggy connait la clé privée
    peggy ->> peggy:Déchiffre le PMS avec sa clé privée

    par Calcul des clés par Peggy
      peggy ->> peggy:Calcule les autres clés à partir du PMS
    and Calcul des clés par Victor
      victor ->> victor:Calcule les autres clés à partir du PMS
    end

    alt Peggy envoie en premier à Victor un message de vérification des échanges
      peggy ->> peggy:Calcule un condensat cryptographique de tous les messages échangés jusqu'ici
      peggy ->> peggy:Protège le condensat en intégrité et confidentialité avec les clés calculées
      peggy ->> victor:"[Chiffré] Voici le condensat de nos échanges"
      victor ->> victor:Calcule un condensat cryptographique de tous les messages échangés jusqu'ici sauf le dernier reçu
      victor ->> victor:Compare le condensat reçu à celui qu'il a calculé
      alt Les condensats ne correspondent pas
        victor ->> peggy: "Je ne crois pas, non."
      else Les condensats correspondent
        victor ->> victor:Calcule un condensat cryptographique de tous les messages échangés jusqu'ici
        victor ->> victor:Protège le condensat en intégrité et confidentialité avec les clés calculées
        victor ->> peggy:"[Chiffré] Voici le condensat de nos échanges"
        peggy ->> peggy:Calcule un condensat cryptographique de tous les messages échangés jusqu'ici sauf le dernier reçu
        peggy ->> peggy:Compare le condensat reçu à celui qu'il a calculé
        alt Les condensats correspondent
          peggy -> victor:"[Chiffré] Données métier"
        else Les condensats ne correspondent pas
          peggy ->> victor: "Je ne crois pas, non."
        end

      end
    else Victor envoie en premier à Peggy un message de vérification des échanges
      victor ->> victor:Calcule un condensat cryptographique de tous les messages échangés jusqu'ici
      victor ->> victor:Protège le condensat en intégrité et confidentialité avec les clés calculées
      victor ->> peggy:"[Chiffré] Voici le condensat de nos échanges"
      peggy ->> peggy:Calcule un condensat cryptographique de tous les messages échangés jusqu'ici sauf le dernier reçu
      peggy ->> peggy:Compare le condensat reçu à celui qu'il a calculé

      alt Les condensats ne correspondent pas
        peggy ->> victor: "Je ne crois pas, non."
      else Les condensats correspondent
        peggy ->> peggy:Calcule un condensat cryptographique de tous les messages échangés jusqu'ici
        peggy ->> peggy:Protège le condensat en intégrité et confidentialité avec les clés calculées
        peggy ->> victor:"[Chiffré] Voici le condensat de nos échanges"
        victor ->> victor:Calcule un condensat cryptographique de tous les messages échangés jusqu'ici sauf le dernier reçu
        victor ->> victor:Compare le condensat reçu à celui qu'il a calculé
        alt Les condensats correspondent
          victor -> peggy:"[Chiffré] Données métier"
        else Les condensats ne correspondent pas
          victor ->> peggy: "Je ne crois pas, non."
        end
      end
    end
  end

Avec Kerberos, le mot de passe du prouveur est utilisé pour déchiffrer une clé symétrique envoyée par le serveur d’authentification (authentication server (AS)). Cette clé est ensuite utilisée par le prouveur pour chiffrer un message à destination du serveur d’émission de tickets (ticket granting service (TGS)). Ce serveur s’attend à ce que le message soit chiffré avec une clé particulière : celle envoyée chiffrée au prouveur par le serveur d’authentification. Ce mécanisme d’authentification est un peu plus complexe qu’expliqué plus haut, car le rôle de vérificateur est, en fait, endossé par deux acteurs distincts : le serveur d’authentification d’une part, et le serveur d’émission de tickets d’autre part.

---
title: Authentification Kerberos (simplifiée)
---
sequenceDiagram
  autonumber
  actor peggy as Peggy (Client)
  box Serveur(s)
    actor trent as Trent (Serveur d'authentification)
    actor victor as Victor (Serveur d'émission de tickets)
    participant db as Base de données
  end

  peggy ->> trent:"Je suis Peggy.".
  trent ->> db:"C'est quoi la dérivée du mot de passe de Peggy ?"
  db ->> trent:"La dérivée du mot de passe de Peggy est..."
  trent ->> db:"C'est quoi la clé de Victor ?"
  db ->> trent:"La clé de Victor est..."
  trent ->> trent:Tire une clé A
  trent ->> peggy:"[Chiffré avec la dérivée du mot de passe de Peggy] Voici la clé A.
  trent ->> peggy:"[Chiffré avec la clé de Victor] Elle dit s'appeler Peggy. Ce message est valide M secondes/minutes/heures. Voici la clé A."
  peggy ->> peggy:Dérive son mot de passe
  peggy ->> peggy:Déchiffre la clé A avec la dérivée de son mot de passe
  peggy ->> victor:"Trent m'a donné ce message pour toi."
  victor ->> victor:Déchiffre le message de Trent avec sa propre clé
  victor ->> victor:Vérifie la période de validité de ces informations
  alt La période de validité est expirée
    victor ->> peggy:"Je ne crois pas, non."
  else La période de validité est en cours
    peggy ->> victor:"[Chiffré avec la clé A] "Je suis Peggy. Je veux accéder au serveur X."
    victor ->> victor:Déchiffre le message Peggy avec la clé A
    victor ->> victor:Vérifie que le nom Peggy figure bien dans les deux messages déchiffrés
    alt Peggy ne connait pas véritablement la clé A, car c'était le mauvais mot de passe. Son nom ne figure pas dans son message après déchiffrement.
      victor ->> peggy:"Je ne crois pas, non."
    else Peggy connait la clé A. Son nom figure bien dans le message qu'elle a chiffré.
      Note right of victor:Victor continue le protocole avec la partie "autorisation"
    end
  end

La preuve d’identité par déchiffrement est plus complexe à mettre en œuvre et ses assurances de sécurité sont plus faibles que d’autres types de preuves, notamment comme implémentée dans TLS 1.3. En conséquence, cette preuve est de moins en moins utilisée. Il s’agit d’un bel exemple du développement itératif et de l’amélioration continue des protocoles cryptographiques ; beaucoup de ceux conçus dans les années 90 sont désormais écartés au profit de constructions plus récentes et robustes.

La preuve avec divulgation nulle de connaissance

Les mécanismes reposant sur la preuve par divulgation nulle de connaissance (ou sans apport de connaissance) sont assez divers. Le principe commun de tous ces mécanismes est que l’élément secret utilisé par le prouveur n’est à aucun moment divulgué au vérificateur. Ce dernier reçoit la preuve de la connaissance de ce secret, sans obtenir d’information sur le secret !

La divulgation nulle de connaissance semble souvent “magique”. Jean-Jacques Quisquater et Louis Guillou ont publié en 1989 un article de vulgarisation intitulé “Comment expliquer à vos enfants les protocoles sans apport de connaissance”, dont le récit est rapporté et traduit dans un article Wikipedia, et dont la lecture peut se révéler éclairante.31.

Une caractéristique intéressante de la preuve à divulgation nulle de connaissance telle qu’implémentée dans le protocole de Schnorr32, ou dans celui de Guillou-Quisquater33, est que le vérificateur peut être raisonnablement certain de la preuve34 qui lui est apportée par le prouveur, tout en étant parfaitement incapable de convaincre un tiers de la même chose, a posteriori. Cette propriété est particulièrement intéressante dans le cas d’applications préservant la vie privée et nécessitant d’authentifier un acteur tout en pouvant nier de manière plausible avoir des preuves que cette authentification ait eu lieu.

---
title: Illustration du protocole de Feige–Fiat–Shamir (simplifié)
---
sequenceDiagram
  autonumber
  actor p as Peggy
  box
    actor v as Victor
    participant db as Base de données
  end

  p ->> v:"Je suis Peggy."
  v ->> db:"C'est quoi la clé publique de Peggy ?"
  db ->> v:"La clé publique de Peggy est..."
  v ->> p:"Convaincs moi."
  p ->> p: Se munit de sa clé privée S
  loop Jusqu'à ce que Victor soit convaincu ou que Peggy ne calcule pas correctement
    p ->> p:Tire une valeur aléatoire x, et calcule une fonction D(x) = X
    p ->> v:"Voici X"
    v ->> v:Tire à pile ou face
    v --> p:"Voici le résultat du tirage à pile ou face"
    p ->> p:Calcule la fonction F(S, x, pile ou face) = Y
    p ->> v:"Voici le résultat du calcul"
    v ->> v:Vérifie le calcul avec la clé publique de Peggy avec une fonction G(X, Y, clé publique de Peggy)
    alt Le calcul est correct
      v ->> v:Est un peu plus convaincu
    else Le calcul est incorrect
      v ->> p: "Je ne te crois pas. Fin de conversation."
    end
  end

La preuve par calculs répartis

Par proximité avec les protocoles à divulgation nulle de connaissance, il peut être fait mention également de ceux utilisant des fonctions pseudo-aléatoires oublieuses (oblivious pseudo-random functions (OPRF) ; il n’existe pas de traduction faisant autorité).

Les OPRF sont des fonctions impliquant deux parties qui contribuent aux entrées d’une fonction générant un nombre aléatoire. L’une des parties apprend la valeur de sortie de la fonction, sans avoir appris la contribution de l’autre partie ; et l’autre partie n’apprend rien. La RFC 949735 décrit une fonction de cette nature. Cette construction est utilisée dans deux protocoles d’authentification très différents : Privacy Pass36 et Opaque37.

Privacy Pass est un protocole d’authentification permettant de prouver non pas une identité individuelle, mais l’appartenance à un groupe d’individus, sans pouvoir distinguer l’individu. Il s’agit bien d’une authentification : un acteur prouve une identité (de groupe) à un vérificateur, qui, en cas de succès, le reconnait bien comme ayant cette identité (d’appartenance à ce groupe). On pourrait, par exemple, imaginer un service de vidéos à la demande payant, qui authentifierait un acteur sans être capable de tracer ce qu’il visionne ; tout ce qu’il saurait, c’est que l’acteur accèdant à la vidéo est à jour de son abonnement. La cryptographie impliquée est un peu avancée pour ce cours, mais le lecteur intéressé pourra en apprendre plus en consultant les documents du groupe de travail “Privacy Pass” de l’Internet Engineering Task Force (IETF).

Opaque, quant à lui, est un protocole d’authentification de la catégorie des Strong Asymmetric Password-based Authenticated Key Exchange (SaPAKE), et utilise donc un mot de passe pour effectuer l’authentification. Il utilise cependant une OPRF afin de permettre de prouver une identité spécifique sans divulguer d’information sur le mot de passe auprès du vérificateur ! L’OPRF est, en effet, utilisée pour calculer une valeur dérivée du mot de passe qui permet le déverrouillage d’un coffre-fort numérique contenant une clé privée. Cette clé privée est ensuite utilisée pour générer la signature d’un défi, comme dans le cas classique des protocoles à signature de défis. L’intérêt d’avoir utilisé une OPRF est que le vérificateur est impliqué dans chaque tentative de déverrouillage du coffre-fort, ce qui lui permet d’empêcher les attaques par recherche exhaustive et de “verrouiller” le compte associé à cette identité si le nombre d’échecs est trop important ! Une fonctionnalité bien utile qui devrait être implémentée par tous les fournisseurs de gestionnaires de mots de passe en ligne synchronisés dans le cloud… De son côté, Whatsapp a mis en œuvre Opaque pour ses sauvegardes chiffrées de bout-en-bout38.

---
title: Authentification avec le protocole Opaque (simplifiée)
---
sequenceDiagram
  autonumber
  actor p as Peggy
  box
    actor v as Victor
    participant db as Base de données
  end

  p ->> p:Tire une valeur aléatoire R
  p ->> p:Calcule 1/R
  p ->> p:Dérive son mot de passe
  p ->> v:"Je suis Peggy."
  p ->> v:"[Chiffré avec R]La dérivée de mon mot de passe est..."
  v ->> p:"Voici ton coffre-fort."
  v ->> v: Se munit de sa clé S
  v ->> p:"[Chiffré avec S][Chiffré avec R]La dérivée de mon mot de passe est..."
  v ->> v: Tire un nonce
  v ->> p:"Voici un nonce"
  p --> p:Déchiffre le message reçu avec 1/R, ce qui donne "[Chiffré avec S]La dérivée de mon mot de passe est..."
  alt Peggy ne connait pas son mot de passe
    Note right of p:Utiliser "[Chiffré avec S]La dérivée de mon mot de passe est..." comme clé ne permet pas de déchiffrer le coffre-fort. "Peggy" ne peut continuer le protocole.
  else
    p ->> p:Utilise "[Chiffré avec S]La dérivée de mon mot de passe est..." comme clé pour déchiffrer le coffre-fort
    p ->> p:Utilise la clé privée contenue dans le coffre pour signer le nonce
    p ->> v:"Voici le nonce signé"
    v ->> db:"C'est quoi la clé publique de Peggy ?"
    db ->> v:"La clé publique de Peggy est..."
    v ->> v:Vérifie la signature du nonce avec la clé publique de Peggy
    alt La signature est valide
      v ->> p:"Bonjour Peggy!"
    else La signature est invalide
      v ->> p:"Je ne crois pas, non."
    end
  end

Du stockage des éléments authentifiants

Pour effectuer l’authentification d’un acteur, le vérificateur a besoin d’avoir accès à une information permettant de vérifier la preuve d’identité. De la nature de la preuve dépend la nature de l’information à stocker côté vérificateur. Le cas le plus typique est celui du mot de passe, mais il en existe d’autres, comme les secrets permettant la génération des codes temporels (TOTP), des clés publiques ou des certificats.

Protocoles reposant sur de la cryptographie à clé publique

Dans le cas où la preuve consiste en une signature numérique vérifiable avec la cryptographie à clé publique, tous les éléments stockés par le vérificateur sont publics par nature ; la seule protection requise est en intégrité, afin de prévenir un attaquant de remplacer les éléments de vérification par les siens. La confidentialité peut néanmoins être un sujet, en particulier si les clés sont réutilisées auprès de plusieurs vérificateurs (voir la section de ce cours dédiée à la réutilisation des moyens de preuve).

Protocole reposant sur de la cryptographie à clé secrète

La cryptographie à clé secrète ou cryptographie symétrique effectue les opérations de vérification avec la même clé que celle qui a permis de créer la preuve. En conséquence, cette clé doit être stockée en clair, sous une forme réversible, ou dans un équipement de sécurité (par exemple, un TPM, un HSM, ou une carte à puce) permettant sa mise en œuvre sans risque d’extraction, copie ou falsification. Ce dernier cas est malheureusement trop rare.

Étant donné que la compromission en confidentialité du stockage du vérificateur permet ensuite l’usurpation de l’identité des acteurs, les protocoles utilisant de la cryptographie à clé secrète sont à éviter, en particulier s’il s’agit de l’unique facteur d’authentification.

C’est le cas notamment des protocoles utilisant les mécanismes de preuve de type challenge/response, comme le mode “digest” de HTTP, spécifié dans la RFC 761628. C’est également le cas du protocole TOTP, spécifié dans la RFC 623839.

Protocoles reposant sur les mots de passe

Le stockage des mots de passe par le vérificateur est sans doute la problématique liée à l’authentification la plus connue, du fait de la quasi-omniprésence des mots de passe comme méthode d’authentification, des méthodes de stockage qui ont largement évolué à mesure que les techniques d’attaque se sont sophistiquées, et des sanctions de la CNIL qui ont été mises en avant sur ces sujets.

Si le mot de passe de l’utilisateur est stocké en clair par le vérificateur, alors la problématique est la même que lorsqu’est utilisé un protocole reposant sur de la cryptographie à clé secrète.

Une meilleure méthode consiste à stocker non pas le mot de passe lui-même, mais le résultat d’une fonction dite de dérivation au mot de passe. Ce résultat de la dérivée est parfois appelé “empreinte”. Parfois, la fonction de dérivation est appelée “fonction de hachage”, de manière abusive, du fait que nombre de ces dérivations sont faites à l’aide de fonctions de hachage cryptographiques. Il existe de très nombreuses fonctions de dérivation, comme nous le verrons plus loin, avec des niveaux de sécurité assez variables.

Finalement, la meilleure méthode reste encore de ne pas transmettre du tout le mot de passe au vérificateur, mais plutôt d’utiliser le mot de passe pour générer une preuve vérifiable ; cette preuve est alors l’unique élément transmis au vérificateur qui ignore tout du mot de passe de l’acteur ! C’est le cas avec le protocole OPAQUE présenté précédemment dans ce cours.

Concernant la dérivation des mots de passe, les principales attaques sont :

  • la recherche exhaustive ou guidée (bruteforce) ;
  • le précalcul ;
  • le “bourrage” de mots de passe (credential stuffing)40 ;
  • “l’épluchage” de mots de passe (credential shucking)41.

Pour ce qui est de la recherche exhaustive ou guidée, si le mot de passe est trop faible, la méthode de dérivation utilisée n’aura pas d’incidence sur la difficulté à retrouver le mot de passe. Même avec la meilleure fonction théoriquement possible, le mot de passe “password” sera cassé presque instantanément. Le guide ANSSI “Recommandations relatives à l’authentification multifacteur et aux mots de passe” indique qu’un mot de passe doit avoir une entropie allant de 65 bits à plus de 100 bits en fonction du niveau de risque associé au compte protégé par ce mot de passe. L’entropie est calculée avec le log_2 du nombre de combinaisons possibles d’un jeu de caractères sur une longueur donnée. Par exemple, si un mot de passe est uniquement numérique, et composé de 10 chiffres, alors l’entropie est de 10 puissance 10, et l’entropie est donc log_2(10^10), soit 33 bits.

De même, si l’acteur utilise le même mot de passe auprès de plusieurs vérificateurs, alors la sécurité du stockage de ce mot de passe est équivalente à la sécurité de la méthode de stockage la plus faible employée par l’un de ces vérificateurs. Si cette méthode de stockage est trop faible, alors le mot de passe peut être recouvré puis utilisé auprès d’un vérificateur ayant pourtant une bonne méthode de stockage ; c’est ce qu’on appelle le “bourrage” de mots de passe. Dans ce cas, peu importe la fonction de dérivation employée : le mot de passe peut être cassé en une seule tentative, puisque le mot de passe est en fait déjà connu.

Pour freiner la recherche exhaustive, il convient d’utiliser une fonction de dérivation qui soit raisonnablement couteuse à calculer. Plus la fonction est couteuse, plus il faudra de temps pour retrouver le mot de passe à partir de sa dérivée. Il y a cependant un arbitrage à faire, car pour chaque tentative de vérification d’un mot de passe, le vérificateur devra utiliser cette fonction couteuse également. S’il y a peu d’utilisateurs, comme c’est le cas pour une authentification locale sur un poste de travail, cela ne présente pas un gros problème, mais sur un site web avec des millions d’utilisateurs et d’utilisatrices, le cout peut rapidement devenir prohibitif.

Le précalcul est une attaque qui vise à précalculer un grand nombre de dérivées de mots de passe, et d’indexer efficacement le résultat de ces calculs. Cet index permet d’optimiser le temps nécessaire pour casser les mots de passe dont les dérivées auraient fuité. Plus besoin d’exécuter la fonction de dérivation pour chaque mot de passe à attaquer ! Il suffit de regarder si la dérivée est présente dans l’index, et si oui de regarder quel mot de passe lui correspond. Ces index sont appelés des tables arc-en-ciel (rainbow tables).

Pour contrer le précalcul, une donnée aléatoire spécifique à chaque compte de chaque système d’information doit être ajoutée au mot de passe en entrée de la fonction de dérivation. Cette donnée est parfois appelée “sel” ou “diversificateur”. Plus cette donnée contient d’entropie, plus le cout du précalcul devient élevé. Le guide ANSSI “Recommandations relatives à l’authentification multifacteur et aux mots de passe” préconise une donnée d’au moins 128 bits d’entropie !

Finalement, l’attaque par épluchage des mots de passe consiste à exploiter le fait que la fonction de dérivation soit en fait une composition de plusieurs fonctions de dérivation unitairement moins fortes que leur assemblage. L’exploitation consiste alors à attaquer chacune de ces fonctions individuellement ; si certaines sont trop faibles, alors l’attaquant peut accélérer d’autant sa recherche exhaustive. C’est par exemple ce qui a été reproché par la CNIL à Doctissimo, qui avait utilisé un assemblage MD5+bcrypt.

La fonction de dérivation de mots de passe à l’état de l’art est argon2id. Cette fonction dérive les mots de passe de telle façon que les calculs ne puissent être faits sur des cartes graphiques. Cette propriété est importante, car les recherches exhaustives ont longtemps été faites sur des cartes graphiques, disposant de processeurs capables d’effectuer de très grandes quantités de certains types de calcul, bien supérieures aux processeurs généralistes. Ensuite, argon2id peut consommer de grandes quantités de mémoire lors de la dérivation des mots de passe, ce qui permet d’accroitre encore le cout d’une recherche exhaustive. Finalement, argon2id met en œuvre un certain nombre de précautions pour éviter des attaques par canaux auxiliaires rendus possibles à cause des techniques permettant de faire consommer plus de mémoire : une protection sur la protection, en somme !

Il existe d’autres fonctions, moins efficaces que argon2id, qui peuvent être mentionnées. Par ordre décroissant de protection, il peut être cité : scrypt, bcrypt, PBKDF2. Toutes ces fonctions, comme argon2id, intègrent toutes par conception un diversificateur pour contrer les précalculs.

De la réutilisation des moyens de prouver son identité

L’utilisation d’un même moyen permettant de prouver son identité n’est pas recommandée. Les conséquences sont cependant assez diverses en fonction du moyen.

La réutilisation d’un moyen dans le cadre d’un mécanisme d’authentification par divulgation a des conséquences catastrophiques si le canal de communication ou la sécurité logicielle du prouveur et du vérificateur sont insuffisamment protégés en confidentialité. En effet, l’attaquant ou l’attaquante pourra alors apprendre le secret et le réutiliser en l’état auprès d’un autre vérificateur.

La réutilisation de mots de passe pose également des difficultés relatives au stockage par le vérificateur. Le stockage du mot de passe lui-même, en clair ou sous un format réversible, signifie que l’attaquant ou l’attaquante ayant accès à ce stockage peut apprendre le mot de passe et le réutiliser tel quel auprès d’autres vérificateurs. Même si le stockage ne contient que des dérivées des mots de passe, le risque reste réel si l’entropie de ces mots de passe est trop faible. En effet, ils pourront être retrouvés par recherche exhaustive, avec les mêmes conséquences que s’ils avaient été stockés en clair.

Avec les mécanismes d’authentification utilisant la signature électronique, l’utilisation d’une même clé privée auprès de plusieurs vérificateurs présente également des risques.

D’une part, il arrive que des erreurs d’implémentation permettent à un vérificateur malveillant d’exploiter une vulnérabilité dans le code du prouveur, menant à la divulgation de la clé privée utilisée pour prouver son identité 4243.

D’autre part, un attaquant ou une attaquante ayant pu obtenir la liste des clés publiques associées à des identités auprès de divers vérificateurs pourrait relier les identités entre elles. Il ou elle lui serait alors possible de tracer et corréler les activités sur les systèmes d’information accessibles après authentification auprès de ces vérificateurs44.

Considérations de sécurité relatives aux moyens de prouver son identité

Fort de toutes les connaissances évoquées dans ce chapitre, il peut sembler difficile de faire le bon choix. Quelles normes, référentiels ou lois s’appliquent ? Quelle force ? Quel protocole ? Faut-il utiliser plusieurs facteurs ? Si oui, lesquels ? Quelles assurances le stockage du vérificateur doit-il fournir ? Quelles assurances le protocole de transport réseau doit-il fournir ? Faut-il utiliser des facteurs physiques comme des cartes à puce ? Si oui, à quel cout ? Qu’utiliser quand on accède à une API ?

Pour ajouter à la complexité technique, il faut ajouter celles relatives aux humains. Quelle solution est la plus acceptable et réaliste ? Est-il réaliste de demander à un humain de mémoriser des mots de passe de plus de 12, voire 16 caractères, générés aléatoirement par une machine et sur 4 classes de caractères ? Que faire si un facteur est perdu ? Comment tenir compte des méthodes de hameçonnage et d’ingénierie sociale, et quel risque leur associer ?

En 2024, les capacités de calcul sont telles que la plupart des humains sont incapables de mémoriser un mot de passe ou une phrase de passe résistant à une attaque par recherche exhaustive, en particulier si la méthode de stockage des dérivées des mots de passe côté vérificateur est insuffisante. Les mauvaises pratiques du milieu45 et la complexité d’analyse d’une adresse réticulaire (Uniform Resource Locator (URL)) sont telles que même les spécialistes peinent à distinguer un lien légitime d’un lien redirigeant vers un site d’hameçonnage 46.

Ces conclusions ont mené les industries sensibles à déconsidérer les facteurs d’authentification basés sur la connaissance (mots de passe, codes PIN, jetons d’authentification), nécessitant un stockage assurant la confidentialité (HTTP digest, etc.) et ceux qui sont vulnérables aux attaques par rejeu ou par relais (mécanisme de preuve par divulgation (dont les TOTP), codes TAN, clés privées réutilisées auprès de plusieurs vérificateurs…). D’un autre côté, rares sont les organismes qui disposent du budget nécessaire pour fournir à tous leurs utilisateurs et utilisatrices des facteurs de possession tels que des cartes à puce, des clés USB implémentant U2F ou FIDO2, ou même des téléphones portables (Duo Push, Microsoft Authenticator). Ces moyens sont généralement réservés au public sensible, comme les équipes de sécurité et les équipes opérationnelles et d’administration.

Pour ces raisons, des mécanismes d’authentification reposant sur le standard W3C WebAuthn47 et utilisant des stockages logiciels plutôt que physiques (soft tokens) ont émergé, avec l’avènement des passkeys en 2023, principalement sous l’impulsion de Google, Apple et Microsoft.

Les passkeys sont des clés utilisées dans le cadre du protocole WebAuthn. Ce protocole prévoit qu’une clé privée distincte soit utilisée par chaque vérificateur (relying party dans la norme). Pour cela, WebAuthn utilise un identifiant unique (Uniform Resource Identifier (URI)) pour distinguer les vérificateurs, le cas typique étant l’adresse réticulaire (URL) du site web demandant une authentification. Chacune de ces clés privées est stockée dans un coffre-fort auquel le prouveur pourra avoir accès sur demande. Pour éviter l’inconvénient de la perte, ce coffre-fort est généralement synchronisé dans le cloud ; c’est le cas, par exemple, pour les passkeys gérées par Apple (stockage dans iCloud) ou par Bitwarden (stockage dans le coffre-fort de mots de passe). L’utilisateur ou l’utilisatrice n’a alors qu’à s’authentifier auprès du service qui stocke ce coffre-fort pour récupérer l’ensemble de ses passkeys.

Contrairement aux mots de passe qui pourraient être stockés dans ces mêmes coffres-forts, les passkeys sont par conception :

  • un mécanisme d’authentification forte, employant des clés cryptographiques ;
  • un mécanisme d’authentification multifacteur, puisqu’un ou plusieurs facteurs d’authentification sont nécessaires pour déverrouiller le coffre-fort contenant les passkeys ;
  • unique à chaque vérificateur, sans réutilisation, ce qui les immunise aux attaques par relais48 et aux hameçonnages.

Contrairement aux facteurs de possession physiques, les passkeys sont par conception :

  • immunisées à la perte ;
  • peu onéreuses.

Enfin, les passkeys étant uniques à chaque vérificateur, elles offrent des propriétés intéressantes pour la vie privée. En effet, il n’existe aucun lien entre deux clés privées distinctes. Cela empêche donc plusieurs vérificateurs de se liguer en vue de tracer et corréler les activités d’un même utilisateur ou d’une même utilisatrice sur leurs systèmes d’information respectifs.

---
title: Authentification avec WebAuthn (simplifiée)
---
sequenceDiagram
  autonumber
  box Client
    actor p as Peggy
    participant cf as Coffre-fort
    participant b as Navigateur
  end
  box Serveur
    participant v as Victor
    participant db as Base de données
  end

  p ->> b:"Je suis Peggy. Je veux accéder à https://broken-by-design.fr/"
  b --> v:"Je suis Peggy. Je veux accéder à https://broken-by-design.fr/"
  v ->> v:Tire un nonce
  v ->> b:"Prouve le. Voici un nonce."
  p ->> cf:Déverrouille son coffre-fort, par exemple avec un mot de passe
  b ->> cf:"Je visite https://broken-by-design.fr et voici le nonce"
  cf ->> cf:Vérifie que Peggy a déverrouillé le coffre-fort il y a peu (*user verification*)
  cf ->> cf:Récupère la clé associée à https://broken-by-design.fr/
  cf ->> cf:Signe le nonce et le site visité
  cf ->> b:"Voici la signature"
  b ->> v:"Voici la signature"
  v ->> db:"C'est quoi la clé publique de Peggy ?"
  db ->> v:"Voici la clé publique de Peggy"
  v ->> v: Vérifie la signature
  alt La signature est valide
    v ->> b:"Bonjour Peggy!"
    b ->> p:"Bonjour Peggy!"
  else La signature est invalide
    v ->> b: "Je ne pense pas, non."
    b ->> p: "Je ne pense pas, non."
  end

Autorisation

L’autorisation est le fait d’accorder ou non, à un sujet (c’est-à-dire un acteur, une organisation, ou un groupe), à l’issue d’un contrôle d’accès, les droits, permissions ou privilèges, lui permettant d’effectuer une activité sur un objet ou une vue.

Derrière cette définition assez générique se cachent des réalités simples et intuitives, mais aussi des modèles de sécurité permettant l’application de politiques plus ou moins abstraites (modèles de Bell-Lapadula, Biba, Clark-Wilson, Multilevel Security (MLS)…). Ce chapitre détaillera ces différents modèles.

Étiquetage explicite ou intrinsèque

Les sujets, actions et objets soumis à un système de contrôle d’accès ont besoin d’être identifiés de manière à pouvoir y faire référence lors de l’écriture de règles de contrôle d’accès.

Pour les sujets, nous avons déjà discuté assez largement de la manière de les identifier dans ce cours.

Pour ce qui est des objets, l’identification peut être effectuée de manière explicite, par l’ajout d’une étiquette (label). Cette étiquette peut décrire une identité individuelle pour cet objet. Néanmoins, la plupart du temps, il s’agit plutôt d’une identité d’appartenance à un groupe. Par exemple, il pourrait s’agir d’une étiquette “objets correspondant à des données bancaires” ou “objets contenant des données personnelles”.

L’étiquetage s’effectue généralement par l’ajout d’une métadonnée associée à l’objet. Sous Linux, cela s’effectue le plus souvent avec les attributs étendus49.

L’étiquetage est le système le plus répandu d’identification des objets, mais il est également possible d’identifier un objet de manière intrinsèque, par exemple en fonction de son nom ou de sa localisation. Ce système est notamment utilisé par le Linux Security Module (LSM) AppArmor qui définit des politiques de sécurité sur des fichiers en fonction de leur chemin d’accès dans le système de fichiers. Un autre exemple est le contrôle d’accès sur un site web, en fonction de la section “chemin” d’une adresse réticulaire (URL Path).

Contrôle d’accès discrétionnaire (DAC)

La plupart des personnes manipulant un ordinateur sont familières avec le contrôle d’accès discrétionnaire (DAC), même si la désignation leur est inconnue.

Ce type de contrôle d’accès consiste à laisser la liberté au propriétaire d’un objet de déterminer les droits d’accès à ce dernier. C’est le modèle de contrôle d’accès par défaut sous Linux, avec les appels système chown(2) et chmod(2) qui permettent respectivement de changer le propriétaire d’un fichier et les droits (lecture, écriture, exécution) qui sont associés à l’utilisateur propriétaire, à un groupe d’utilisateurs, et aux autres utilisateurs.

Ce contrôle d’accès est parfaitement adapté pour la gestion du contrôle d’accès à des données personnelles. Néanmoins, dans une entreprise, le contrat de travail contient quasi systématiquement une clause de cession des droits patrimoniaux de l’ensemble des productions du personnel. Pour le dire plus vulgairement, l’entreprise est la propriétaire des documents produits. Comme il n’est pas raisonnable d’attendre du chef ou de la cheffe d’entreprise de prendre toutes les décisions d’attribution des droits, des politiques de sécurité sont établies, qui s’appliquent ensuite de manière verticale sur l’ensemble des objets produits par le personnel de l’entreprise. L’étiquetage entre alors en jeu.

Contrôle d’accès obligatoire (MAC)

Le contrôle d’accès obligatoire (Mandatory Access Control) a parfois été incorrectement appelé “contrôle d’accès mandataire” par les agents du gouvernement français, probablement pour des raisons de proximité phonétique avec “mandatory”…

Ce type de contrôle d’accès consiste en l’établissement d’une politique de sécurité qui s’applique de manière verticale à l’ensemble des sujets, des actions et des objets d’un système d’information.

Les politiques sont des descriptions abstraites des relations entre les sujets, les actions et les objets. Bien qu’elles puissent établir des règles à propos de sujets ou d’objets individuels spécifiques, elles portent plus généralement sur des étiquettes pouvant être associées à plusieurs sujets ou objets.

Les modèles théoriques

Les modèles de MAC classiques

La littérature contient de nombreuses variantes et approches au contrôle d’accès obligatoire. Certaines sont assez intuitives, par exemple en attribuant des rôles aux sujets (Role-based Access Control (RBAC)). D’autres approches utilisent des attributs arbitraires associés aux sujets, actions ou objets (Attribute-based Access Control (ABAC)), ou encore des informations contextuelles à propos du sujet (Context-based Access Control (CBAC)) : (heure d’accès, géolocalisation, capacités du terminal, force de l’authentification, etc…).

OrBAC

Certaines approches au contrôle d’accès obligatoires rajoutent des couches d’abstraction supplémentaires, en bâtissant sur ceux déjà cités. Par exemple, le modèle de contrôle d’accès fondé sur l’organisation (Organization-based Access Control (OrBAC)) repose sur les approches par rôles (RBAC), par actions (Task-based Access Control (TBAC)), par vues (View-based Access Control (VBAC)) et par équipes (Team-based Access Control (TBAC, à nouveau…)). Le modèle OrBAC abstrait le triplet “sujets, action, objets” en un nouveau triplet “rôles, activités, vues”.

Des règles de contrôle d’accès peuvent alors être écrites concernant ce nouveau triplet en faisant abstraction du contexte d’implémentation, c’est-à-dire de l’organisation (société, association, etc.) pour laquelle elles sont écrites. Cela permet ainsi de les écrire une fois pour toutes, pour un nombre arbitraire d’organisations qui y auront recours.

Une couche de “traduction” spécifique à chaque organisation doit alors être utilisée pour transposer ces règles abstraites en des règles pragmatiques concernant des acteurs, les actions et les objets de chaque organisation.

Sécurité multiniveau

L’approche par sécurité multiniveau (Multilevel Security (MLS)) est assez ancienne. Elle est particulièrement appréciée dans les domaines militaires et gouvernementaux, du fait de la verticalité intrinsèque de cette approche. Néanmoins, cette approche a également ses mérites en dehors du domaine exécutif. Elle est, par exemple, intéressante pour la centralisation des données de santé (monitoring) et de journalisation d’un système d’information.

Le principe général de la sécurité multiniveau est, en effet, d’établir une hiérarchie de niveaux d’autorisation. Les acteurs associés à un niveau sont alors autorisés à effectuer un jeu d’actions restreint avec les objets associés aux niveaux situés en dessous du niveau de l’acteur. Un autre jeu d’actions, généralement distinct et disjoint, est également autorisé pour cet acteur avec les objets associés aux niveaux situés au-dessus du niveau de l’acteur.

Parmi les modèles de sécurité reposant sur la sécurité multiniveau, on peut notamment citer le modèle de Bell-Lapadula50, le modèle Biba51, ou encore celui de Clark-Wilson52.

Le modèle Bell-Lapadula

Le modèle Bell-Lapadula utilise la sécurité multiniveau à des fins de confidentialité. Le principe général est qu’un acteur situé à un certain niveau de sécurité ne peut qu’écrire aux niveaux supérieurs et ne peut que lire aux niveaux inférieurs. En anglais, on parle d’une approche “Write Up, Read Down (WURD)”. Avec cette approche, les acteurs de la hiérarchie peuvent concentrer les informations confidentielles, sans qu’aucune information notamment agrégée ne puisse redescendre.

Ce modèle est implémenté dans Linux à l’aide de SElinux (Security-Enhanced Linux), qui dispose d’une fonctionnalité multiniveau grâce aux étiquettes de niveaux de sensibilité.

Le modèle Biba

Le modèle Biba utilise la sécurité multiniveau à des fins d’intégrité. Le principe général est comparable à celui de Bell-Lapadula, bien qu’il soit en quelque sorte inversé. Avec le modèle Biba, il n’y pas d’écriture aux niveaux supérieurs, et pas de lecture aux niveaux inférieurs. Ainsi, les niveaux les plus privilégiés sont protégés de toute interférence ou corruption de la part des niveaux inférieurs. En anglais, on parle d’une approche “Read Up, Write Down (RUWD)”.

Ce modèle peut être adroitement utilisé conjointement avec le modèle de Bell-Lapadula afin d’agréger des données confidentielles au niveau du commandement, et permettre ensuite au commandement de faire redescendre des ordres qui sont fonction de ces données agrégées.

Le module de Clark-Wilson

Le modèle de Clark-Wilson s’intéresse également à l’intégrité. Son approche est plus dynamique et modélise le triplet “sujet,programme, objets”. Ce triplet signifie que les sujets ne peuvent manipuler les objets que par des programmes spécifiques. Ceci est plus restrictif que dans les autres modèles où le contrôle d’accès s’effectue en fonction de l’acteur, sans tenir compte de comment il y accède.

Ce modèle est notamment implémentable sous Linux grâce aux politiques de contrôles des types (type enforcement (TE)) de SElinux. Le type enforcement permet d’associer notamment à chaque utilisateur système ou rôle (ce qui est une autre notion de SElinux), à chaque programme exécutable, à chaque fichier, à chaque socket un type. Une politique TE régit ensuite les interactions entre les types (écriture, lecture, exécution, modification des métadonnées, etc.), ainsi que les transitions entre les types : tel utilisateur de type W peut exécuter un programme de type X, ce qui lui permet de lire les fichiers de type Y et d’écrire les fichiers de type Z.

Modèle de Brewer et Nash ou de la muraille de Chine

Le modèle de Brewer et Nash consiste à appliquer une politique de sécurité permettant de prévenir les conflits d’intérêts, notamment en matière commerciale. Dans ce modèle, le contrôle d’accès est dynamique ; par défaut l’acteur a virtuellement accès à toutes les données. Néanmoins, en fonction des données auxquelles il accède, il se voit au fur et à mesure restreindre l’accès à d’autres données. Par exemple, si les sociétés A et B sont concurrentes, accéder aux données de la société A fait que l’on perdra automatiquement les accès aux données de la société B.

Un exemple d’implémentation de ce modèle est l’outil StemJail53 pour Linux, développé par Mickaël Salaün (ANSSI), avec Marion Daubignard (ANSSI) et sous la coulpe d’Hérvé Debar (SudParis Télécom), dans le cadre de la thèse de doctorat de Mickaël.

Des rôles et des groupes

Avec l’approche Role-based Access Control (RBAC), des rôles sont attribués à des sujets. Comme vu précédemment dans ce cours, le terme de sujet recouvre plusieurs réalités : des acteurs mais aussi des groupes d’acteurs. Or, dans certaines définitions de ce que sont les rôles, il est retenu que les rôles sont des groupes d’acteurs et de permissions, tandis que d’autres définitions retiennent que les rôles sont des collections de permissions54. Ainsi, certaines implémentations considérant que les rôles sont des groupes d’acteurs et de permissions n’implémentent pas du tout la notion de groupes d’acteurs (sans permissions), considérant celle-ci redondante avec celle des rôles. Pour rajouter un peu de confusion, certaines implémentations ne nomment pas les rôles “rôles” mais “groupes” (e.g. Gitlab) ou “équipes” (e.g. Gitea/Forgejo).

D’une manière générale, il est fortement déconseillé d’attribuer des permissions à des acteurs de façon nominative, à moins qu’il ne soit possible de placer un commentaire explicite à côté de ces permissions accordées nominativement, afin de justifier leur attribution.

Prenons l’exemple d’un personnel d’entreprise, inspiré d’un cas réel : au cours de sa vie professionnelle au sein de cette entreprise, ce personnel intègre une équipe de développement et obtient les permissions nécessaires à l’accomplissement de son affectation. Plus tard, ce même personnel réoriente sa carrière et devient ingénieur avant-vente, et se voit attribuer en conséquence de nouvelles permissions pour l’accomplissement de sa nouvelle affectation. Finalement, ce personnel bascule totalement dans l’équipe commerciale, et obtient encore de nouvelles permissions. Comme prévu dans la politique de sécurité de la société, les permissions sont auditées annuellement, afin de s’assurer que personne ne détient des permissions indues. Lorsque la personne en charge de l’audit vérifiera les permissions de ce personnel, il lui sera très difficile de s’assurer de la légitimité de chaque permission unitaire sans investigation. Si les permissions avaient été accordées en fonction de l’appartenance ou non à un groupe d’acteurs, il suffirait de contrôler la pertinence de l’appartenance aux groupes des développeurs, des avant-ventes et des commerciaux.

En conséquence, même lorsque la notion de rôles est celle de groupes d’acteurs et de permissions, il est fortement conseillé de créer des rôles composés uniquement d’acteurs et de rôles composés exclusivement de permissions et de créer une hiérarchie entre ces rôles, de façon à ce que les rôles “groupes” héritent des permissions des rôles “permissions”.

OAuth2, OpenID Connect et assertions de sécurité

Les protocoles OAuth2 et OpenID Connect sont détaillés dans cette section en préparation du TP compagnon de ce cours. Il existe d’autres protocoles d’autorisation, comme Kerberos ou SAML, qui ne seront pas abordés dans ce cours, mais qui offrent des propriétés de sécurité intéressantes ou comparables.

OAuth2

OAuth2 est un protocole standard d’autorisation, extensible, principalement utilisé dans le contexte web. Une partie de sa spécification, ce qui est appelé le “cadriciel OAuth2” (framework), repose dans la RFC 674955. Cette RFC est également completée par les RFC 675056 et 825257. Le principe général d’OAuth2 repose sur un protocole impliquant quatre participants :

  • une personne propriétaire d’une ressource (en anglais “resource owner”);
  • un serveur de ressources (en anglais “resource server”);
  • une tierce partie souhaitant obtenir accès à cette ressource ; il s’agit généralement d’une application (en anglais “client” ou “relying party”);
  • un serveur d’autorisation (en anglais “authorization server).
---
title: Représentation simplifiée des interactions entre les parties du protocole OAuth2
---
sequenceDiagram
  autonumber
  actor p as Propriétaire
  actor a as Serveur d'autorisation
  actor r as Serveur de ressources
  actor c as Client

  c ->> r:"Je souhaite accéder à la ressource X."
  r ->> c:"Il faut un jeton d'accès. Va voir le serveur d'autorisation."
  c ->> a:"J'aurais besoin d'un jeton d'accès pour la ressource X. Je redirige le propriétaire vers toi."
  a ->> p:"T'es qui ?"
  p ->> a:"Je suis Peggy, et en voici la preuve."
  a ->> a:Vérifie la preuve.
  a ->> p:"OK, Peggy. J'ai ce Client qui voudrait accéder à X dont tu es propriétaire. T'en penses quoi ?"
  p ->> a:"OK pour donner accès à Client à la ressource X."
  a ->> c:"OK, voici un jeton d'accès pour X."
  c ->> r:"Je souhaite accéder à la ressource X. Voici le jeton d'accès."
  r ->> r:Vérifie le jeton d'accès.
  alt Le jeton d'accès est valide
    r ->> c:"Voici la ressource X."
  else Le jeton d'accès est invalide, forgé ou expiré
    r ->> c:"Accès refusé."
  end

Le serveur de ressources et le serveur d’autorisation ont établi une relation de confiance au préalable. Cette relation de confiance permet au serveur de ressources de reconnaitre et vérifier des jetons d’accès émis par le serveur d’autorisation.

De même, la personne propriétaire d’une ressource a préalablement établi une relation de confiance avec le serveur d’autorisation ; elle est notamment capable de lui prouver son identité, d’une part, et de le convaincre qu’elle est propriétaire d’une certaine donnée, d’autre part.

Finalement, la tierce partie et le serveur d’autorisation ont également préalablement établi une relation de confiance qui permet à la tierce partie de prouver son identité au serveur d’autorisation.

Ainsi, lorsque la tierce partie manifeste le besoin d’accéder à la ressource, elle sollicite le serveur d’autorisation pour obtenir un jeton d’accès et s’authentifie auprès de lui. Après avoir vérifié l’identité de la personne propriétaire de la ressource, le serveur d’autorisation lui demande l’accord pour délivrer un jeton d’accès à cette ressource pour cette tierce partie spécifique. En cas d’accord, un jeton est délivré. La tierce partie peut alors contacter le serveur de ressources et présenter le jeton d’accès. Le serveur de ressources vérifie le jeton d’accès, et s’il est valide, donne accès à la ressource.

Ce protocole permet de délivrer des autorisations d’accès à des ressources protégées, avec une granularité réglable, sans révéler à la tierce partie l’identité ou les moyens d’identification (les “authentifiants”) du propriétaire de la ressource. De surcroit, ce protocole permet de délivrer à la tierce partie des jetons d’accès à usage restreint : ils sont incessibles et potentiellement limités dans le temps.

Une critique récurrente qui peut être adressée contre le cadriciel d’OAuth2 est la multitude de ces modes opératoires, certains étant par essence d’une sécurité relativement douteuse. C’est notamment le cas du mode implicite, ou lorsque sont utilisés des clients “publics”. Ce cours ne rentrera pas plus avant dans ces considérations, mais de plus amples informations peuvent être trouvées dans la RFC de référence, à la section “Considérations de sécurité”.

Une utilisation “abusive” ou “détournée” du protocole d’OAuth2 est souvent constatée pour l’authentification décentralisée. Dans ce contexte d’usage, le serveur de ressources et le serveur d’autorisation sont la même entité. Lorsqu’une personne veut s’authentifier auprès d’une application tierce, elle demande à cette application de demander une autorisation d’accès au serveur d’autorisation. Le serveur d’autorisation demande à cette personne de s’authentifier, puis renvoie à la tierce partie un jeton d’accès pour une ressource quelconque contenant une information identifiant la personne qui s’est authentifiée. Si la tierce partie est en mesure d’obtenir cette ressource grâce au jeton d’accès, alors elle peut déduire que la personne a été capable de prouver son identité au serveur d’autorisation.

Cette utilisation abusive a été normalisée par une extension au protocole OAuth2 : OpenID Connect.

OpenID Connect

OpenID Connect (OIDC)58 est une extension au protocole OAuth2. Elle rajoute notamment une couche d’identité, qui permet de normaliser la communication de l’identité d’une personne connue du serveur OIDC à la tierce partie (appelée en anglais relying party), et de fournir également des informations complémentaires à son sujet. Ces informations sont appelées des assertions (claims), qui peuvent être de nature arbitraire, tant qu’elles sont exprimables dans le format JWT (JSON Web Token)59.

Le principe général d’OAuth 2 est appliqué avec OpenID Connect. La différence majeure est que le serveur d’autorisation est également le serveur de ressources, et que la ressource consultée est un document appelé “ID Token”, contenant des assertions relatives à la personne s’étant authentifiée au serveur d’autorisation.

Le document ID Token contient diverses assertions requises ou optionnelles. Les assertions requises sont :

  • iss : “issuer” : l’identifiant de l’émetteur de cet ID Token ;
  • sub : “subject” : l’identifiant de la personne cherchant à prouver son identité à la tierce partie ;
  • aud : “audience” : l’identifiant de la tierce partie ;
  • exp : “expiry date” : la date d’expiration de cet ID Token ;
  • iat : “issued at” : la date d’émission de cet ID Token.

Les assertions suivantes sont parfois requises, parfois optionnelles :

  • auth_time : “authentication time” : heure de la dernière authentification réussie de la personne cherchant à prouver son identité à la tierce partie auprès du serveur OpenID Connect. Cette assertion est requise si l’application tierce a explicitement exprimé le besoin d’en connaitre à propos cette information.
  • nonce : une valeur arbitraire ajoutée à l’ID Token à la demande de la tierce application, en vue de contrer des attaques par rejeu éventuelles.

Les serveurs OpenID Connect peuvent être généralement configurés pour ajouter des assertions additionnelles arbitraires. Certaines peuvent ainsi spécifier les groupes auxquels appartient le sujet de l’ID Token ou les rôles dont il disposerait.

User-managed Access

User-managed Access (UMA) est une autre extension au protocole OAuth2. Spécifiée par la Kantara Initiative60, elle permet au propriétaire d’une ressource d’établir à l’avance des règles régissant l’émission de jetons d’accès à cette ressource. L’application tierce sera alors autonome pour obtenir des jetons d’accès à cette ressource sous réserve de satisfaction des règles édictées par le propriétaire et sans interaction explicite avec ce dernier.

Cette extension permet notamment la création d’un référentiel central d’autorisation, seule source de vérité des autorisations d’accès sur l’ensemble des ressources d’un système d’information.

Hélas, s’il y a bon nombre de fournisseurs de service pour le protocole UMA, son adoption par les applications tierces est encore modeste.

Remerciements

Je tiens à remercier mes relecteurs et relectrices pour leurs contributions à ce cours. Un merci tout spécial à @karl@infosec.exchange et @bortzmeyer@mastodon.gougere.fr pour leurs suggestions d’amélioration nombreuses et détaillées.

Le contenu de ce cours ne saurait les engager.

Licence

Ce cours est publié sous licence CC-BY.

Vous êtes autorisé à :

  • Partager — copier, distribuer et communiquer le matériel par tous moyens et sous tous formats pour toute utilisation, y compris commerciale.
  • Adapter — remixer, transformer et créer à partir du matériel pour toute utilisation, y compris commerciale.

L’Offrant ne peut retirer les autorisations concédées par la licence tant que vous appliquez les termes de cette licence.

Selon les conditions suivantes :

  • Attribution — Vous devez créditer l’Œuvre, intégrer un lien vers la licence et indiquer si des modifications ont été effectuées à l’Œuvre. Vous devez indiquer ces informations par tous les moyens raisonnables, sans toutefois suggérer que l’Offrant vous soutient ou soutient la façon dont vous avez utilisé son Œuvre.
  • Pas de restrictions complémentaires — Vous n’êtes pas autorisé à appliquer des conditions légales ou des mesures techniques qui restreindraient légalement autrui à utiliser l’Œuvre dans les conditions décrites par la licence.

  1. https://github.com/linux-pam/linux-pam ↩︎

  2. https://www.rfc-editor.org/rfc/rfc4511.html ↩︎

  3. https://learn.microsoft.com/fr-fr/windows-server/identity/ad-ds/plan/using-the-organizational-domain-forest-model ↩︎

  4. https://en.wikipedia.org/wiki/Virtual_directory ↩︎

  5. https://openid.net/developers/how-connect-works/ ↩︎

  6. https://wiki.oasis-open.org/security/FrontPage ↩︎

  7. https://cyber.gouv.fr/secnumcloud-pour-les-fournisseurs-de-services-cloud ↩︎ ↩︎

  8. version 3.2, chapitre 9.6, alinéa a ↩︎

  9. https://www.legifrance.gouv.fr/cnil/id/CNILTEXT000046693390 ↩︎

  10. https://www.legifrance.gouv.fr/cnil/id/CNILTEXT000042203965/ ↩︎

  11. https://www.legifrance.gouv.fr/cnil/id/CNILTEXT000047552103 ↩︎

  12. https://eur-lex.europa.eu/legal-content/FR/TXT/?uri=CELEX:32014R0910 ↩︎

  13. https://eur-lex.europa.eu/legal-content/FR/TXT/?uri=celex%3A32015L2366 ↩︎

  14. https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000035430451 ↩︎

  15. https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000035407334/ ↩︎

  16. https://ec.europa.eu/digital-building-blocks/sites/display/DIGITAL/eIDAS+eID+Profile ↩︎

  17. https://eur-lex.europa.eu/legal-content/FR/ALL/?uri=CELEX:32015R1502 ↩︎ ↩︎

  18. https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000035720606 ↩︎

  19. https://cyber.gouv.fr ↩︎

  20. https://cyber.gouv.fr/lidentification-electronique ↩︎

  21. https://cyber.gouv.fr/le-referentiel-general-de-securite-version-20-les-documents ↩︎

  22. https://www.cloudflare.com/fr-fr/learning/bots/what-is-credential-stuffing/ ↩︎

  23. https://jankrissler.blogspot.com/2016/09/hacker-fakes-german-ministers.html ↩︎

  24. https://en.wikipedia.org/wiki/Trusted_Platform_Module ↩︎

  25. https://en.wikipedia.org/wiki/Hardware_security_module ↩︎

  26. https://www.lemagit.fr/conseil/Key-Management-System-KMS-une-pierre-angulaire-du-chiffrement ↩︎

  27. https://heartbleed.com/ ↩︎

  28. https://www.rfc-editor.org/rfc/rfc7616.html ↩︎ ↩︎

  29. Une des propriétés de sécurité attendues des fonctions de hachage cryptographiques est qu’il soit virtuellement impossible de déterminer, autrement que par la recherche exhaustive, une valeur qui, passée à la fonction de hachage en entrée, produirait un résultat connu. ↩︎

  30. Le serveur peut également stocker une dérivée du mot de passe, mais pour ce protocole, cette dérivée de mot de passe a la même sensibilité que le mot de passe lui-même : le connaitre est suffisant pour s’authentifier. ↩︎

  31. https://fr.wikipedia.org/wiki/Preuve_%C3%A0_divulgation_nulle_de_connaissance#La_grotte_d'Ali_Baba ↩︎

  32. https://fr.wikipedia.org/wiki/Protocole_d%27authentification_de_Schnorr ↩︎

  33. https://fr.wikipedia.org/wiki/Guillou-Quisquater ↩︎

  34. Les protocoles à divulgation nulle de connaissance sont des protocoles probabilistes. Le vérificateur ne dispose jamais d’une preuve formelle de l’identité ; il acquiert juste une conviction reposant sur une probabilité écrasante que le prouveur connait ou possède le secret prouvant son identité. Le seuil de probabilité à partir duquel il est convaincu est paramétrable et à la discrétion du vérificateur. ↩︎

  35. https://www.rfc-editor.org/rfc/rfc9497.html ↩︎

  36. https://datatracker.ietf.org/wg/privacypass/documents/ ↩︎

  37. https://eprint.iacr.org/2018/163.pdf ↩︎

  38. https://www.whatsapp.com/security/WhatsApp_Security_Encrypted_Backups_Whitepaper.pdf ↩︎

  39. https://www.rfc-editor.org/rfc/rfc6238.html ↩︎

  40. https://www.cloudflare.com/fr-fr/learning/bots/what-is-credential-stuffing/ ↩︎

  41. https://www.scottbrady91.com/authentication/beware-of-password-shucking ↩︎

  42. https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0777 ↩︎

  43. https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-31497 ↩︎

  44. https://words.filippo.io/dispatches/whoami-updated/ ↩︎

  45. Il semble très difficile d’astreindre les départements marketing et communications à utiliser uniquement des noms de domaine bien identifiés, que les utilisateurs et utilisatrices finaux sont habitués à reconnaitre… ↩︎

  46. https://github.com/orangetw/My-Presentation-Slides/blob/main/data/2019-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdfs ↩︎

  47. https://www.w3.org/groups/wg/webauthn/publications/ ↩︎

  48. Le standard WebAuthn requiert la vérification de l’identité du vérificateur. Cette vérification est généralement faite à l’aide de TLS et de l’infrastructure de gestion de clés “web”. Si un attaquant réussit à disposer de la clé privée associée à un certificat légitime pour un vérificateur donné, alors il sera en mesure d’effectuer tout de même des attaques relais. WebAuthn dispose d’une mécanique appelée “channel binding” ou “token binding” permettant de prévenir totalement les attaques par relais, mais cette fonctionnalité n’a jamais été implémentée à large échelle. ↩︎

  49. https://man7.org/linux/man-pages/man7/xattr.7.html ↩︎

  50. https://websites.umich.edu/~cja/LPS12b/refs/belllapadula1.pdf ↩︎

  51. https://www.researchgate.net/publication/235043659_Integrity_Considerations_for_Secure_Computer_Systems ↩︎

  52. https://groups.csail.mit.edu/ana/Publications/PubPDFs/A%20Comparison%20of%20Commercial%20and%20Military%20Computer%20Security%20Policies.pdf ↩︎

  53. https://stemjail.github.io/ ↩︎

  54. https://profsandhu.com/workshop/role-group.pdf ↩︎

  55. https://www.rfc-editor.org/rfc/rfc6749.html ↩︎

  56. https://www.rfc-editor.org/rfc/rfc6750.html ↩︎

  57. https://www.rfc-editor.org/rfc/rfc8252.html ↩︎

  58. https://openid.net/developers/specs/ ↩︎

  59. https://www.rfc-editor.org/rfc/rfc7519.html ↩︎

  60. https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html ↩︎

Identité et méthodes d'authentification : TP

TP

Ce TP consiste à mettre en place un référentiel d’identités et un service qui utilisera ce référentiel pour effectuer l’authentification décentralisée de ces utilisateurs et utilisatrices. Ce service sera la forge logicielle Forgejo et le référentiel d’identités sera Keycloak.

Le référentiel d’identités sera configuré pour permettre l’authentification sans mot de passe, à l’aide de WebAuthn et de passkeys.

Pour stocker et gérer les passkeys, nous aurons recours à Bitwarden et son extension navigateur d’une part et à un serveur Vaultwarden d’autre part. Vaultwarden est un serveur libre compatible avec l’API de Bitwarden. Il nous permet d’effectuer ce TP sans “polluer” les serveurs Bitwarden officiels avec des comptes créés pour ce TP.

L’authentification avec WebAuthn nécessite que les sites soient servis sur HTTPS. Pour cette raison, nous allons mettre en place un serveur HTTPS qui tiendra lieu de serveur mandataire inverse (reverse proxy).

Ajouter une adresse IP dédiée au TP

(Action) Ajouter une adresse IP locale dédiée pour le TP. Bloquer le cas échéant les connexions depuis le réseau externe.

Sur Linux, ajouter une interface de type dummy et configurer une adresse IP dédiée pour le TP. Par exemple, 10.108.0.1.

sudo ip l a dummy0 type dummy
sudo ip a a 10.108.0.1 dev dummy0
sudo ip l s dummy0 up

Sur Windows et Mac, trouver une manière de faire…

Installation du serveur mandataire inverse

Avant toute chose, il convient de configurer le serveur mandataire, et de configurer notre navigateur internet afin qu’il fasse confiance aux certificats émis par ce serveur mandataire. Pour ce faire, nous allons utiliser Caddy.

Caddy est un serveur qui peut à la fois jouer le rôle de serveur HTTP, mais aussi assurer la gestion d’une infrastructure de gestions de clés (IGC), et même délivrer des certificats à l’aide du protocole ACME1.

Le conteneur Caddy a besoin d’exposer le port 443. Il stocke ses fichiers relatifs à l’IGC dans un volume qui doit être monté sur le chemin /data. Finalement, sa configuration est effectuée avec un fichier “Caddyfile”, situé au chemin /etc/caddy/Caddyfile.

La syntaxe des Caddyfile est documentée sur le site officiel de Caddy2.

Pour le moment, nous allons simplement le déclarer et le démarrer pour obtenir le certificat racine de l’infrastructure de gestion de clés.

(Action) Créer un répertoire caddy dans votre répertoire de travail pour ce TP.

(Action) Écrire un fichier de configuration Caddyfile minimaliste et le stocker dans caddy/Caddyfile.

{
    http_port 80   # écoute sur le port 80
    https_port 443  # écoute sur le port 443
    admin off  # désactivation de l'API qui n'est pas utilisée dans ce TP
    log {
        output stdout  # journalisation sur stdout afin de le récupérer par le driver de log de l'engine
        format console  # journalisation au format console pour faciliter la lisibilité des logs (pas recommandé en production)
        level info  # journalisation de niveau info, pour avoir des informations, mais pas trop
    }
    local_certs  # tous les certificats sont émis par l'IGC interne, au lieu d'aller les chercher sur Internet
    skip_install_trust  # n'effectue pas une étape inutile quand on fait tourner Caddy dans un conteneur
}

(Action) Étendre l’image officielle de Caddy pour ajouter ce fichier de configuration. Pour cela, créer le fichier Containerfile (ou Dockerfile) suivant, et le stocker dans le répertoire caddy.

FROM docker.io/caddy:2.7.6-alpine
COPY Caddyfile /etc/caddy

(Action) Créer un fichier podman-compose.yml ou docker-compose.yml qui inclura les différentes définitions fournies ultérieurement

version: '3.7'

(Action) Déclarer dans le fichier compose un conteneur caddy

Le réseau caddy_frontend est le réseau public sur lequel seront exposés les ports 80 et 443. Le réseau caddy_backend est le réseau privé sur lequel seront connectés les différents services rendus disponibles par Caddy.

volumes:
  caddy_data:
    driver: local

networks:
  caddy_frontend:
    name: caddy_frontend
    internal: false
  caddy_backend:
    name: caddy_backend
    internal: true

services:
  caddy:
    build: caddy/
    container_name: caddy
    restart: always
    ports:
      - 8080:80
      - 8443:443
    volumes:
      - caddy_data:/data
    networks:
      - caddy_frontend
      - caddy_backend

(Action) Démarrer le conteneur

Depuis un shell, taper :

podman compose -f podman-compose.yml up -d
docker compose -f docker-compose.yml up -d

(Action) Récupérer le certificat racine

Depuis un shell, taper :

podman cp caddy:/data/caddy/pki/authorities/local/root.crt caddy.crt
docker cp caddy:/data/caddy/pki/authorities/local/root.crt caddy.crt

(Action) Insérer le certificat racine dans le magasin de certificat de son navigateur.

Avec Firefox, cela peut se faire de manière programmatique, avec l’utilitaire certtool. Cet utilitaire peut être installé avec le paquet “libnss3-tool” sur Debian/Ubuntu, et le paquet “nss-tools” sur Fedora.

Ensuite, exécuter la commande suivante dans un shell :

find $HOME -type f -name "cert9.db" | while read filename ; do certutil -A -n "CA TP" -t "TC" -i $PWD/caddy.crt -d "$(dirname "$filename")" ; done

Installation de Vaultwarden

Vaultwarden est un serveur compatible avec l’API Bitwarden. Il permet le stockage de mots de passe et de passkeys.

Il va être installé sous la forme d’un conteneur.

(Action) Ajouter le nom de domaine vault.tp-authn.broken-by-design.fr dans le fichier hosts, et le faire pointer vers 10.108.0.1

Sur Linux et Mac, éditer le fichier /etc/hosts. Sur Windows, éditer le fichier C:\Windows\system32\drivers\etc\hosts.

10.108.0.1 vault.tp-authn.broken-by-design.fr

(Action) Ajouter dans le fichier compose la déclaration des ressources pour un serveur Vaultwarden

Dans la section volumes, ajouter :

  vault_data:
    driver: local

Dans la section services, ajouter :

  vault:
    image: ghcr.io/dani-garcia/vaultwarden:1.30.5-alpine
    container_name: vault
    restart: always
    volumes:
      - vault_data:/data
    networks:
      - caddy_backend
    environment:
      SIGNUPS_ALLOWED: true
      SIGNUPS_DOMAINS_WHITELIST: tp-authn.broken-by-design.fr
      INVITATIONS_ALLOWED: false
      DOMAIN: https://vault.tp-authn.broken-by-design.fr:8443
      SHOW_PASSWORD_HINT: false
    depends_on:
      - caddy

(Action) Ajouter l’hôte virtuel pour Vaultwarden dans la configuration de Caddy

vault.tp-authn.broken-by-design.fr:80 {
  redir vault.tp-authn.broken-by-design.fr:8443{uri}
  log
}
vault.tp-authn.broken-by-design.fr:443 {
  reverse_proxy vault:80
  log
}

(Action) Détruire, reconstruire et recréer les conteneurs

podman compose -f podman-compose.yml down
podman compose -f podman-compose.yml build
podman compose -f podman-compose.yml up -d
docker compose -f docker-compose.yml down
docker compose -f docker-compose.yml build
docker compose -f docker-compose.yml up -d

(Action) Ouvrir dans son navigateur https://vault.tp-authn.broken-by-design.fr:8443/ et se créer un compte

(Action) Installer l’extension navigateur Bitwarden

(Action) Se connecter à son compte Vaultwarden dans l’extension Bitwarden. Penser à sélectionner “Connexion sur auto-hébergé” et renseigner l’URL du serveur “https://vault.tp-authn.broken-by-design.fr:8443/"

Installation de la forge logicielle

La forge logicielle est installée avec un conteneur. Ce conteneur dispose d’un volume /data pour le stockage de la configuration et des données des utilisateurs et utilisatrices.

Nous utiliserons une base de données sqlite3 pour stocker les données. En production, il est recommandé d’utiliser un gestionnaire de base de données plus robuste, comme Postgresql ou MariaDB.

Nous avons d’abord besoin de déclarer le nom de domaine qui sera utilisé pendant ce TP pour joindre la forge.

(Action) Ajouter le nom de domaine git.tp-authn.broken-by-design.fr dans le fichier hosts, et le faire pointer vers 10.108.0.1

Sur Linux et Mac, éditer le fichier /etc/hosts. Sur Windows, éditer le fichier C:\Windows\system32\drivers\etc\hosts.

10.108.0.1 git.tp-authn.broken-by-design.fr

Plus tard dans le TP, nous aurons besoin que Forgejo contacte le référentiel d’identités. Comme ce dernier sera rendu accessible par Caddy, il nous faut ajouter le certificat racine de Caddy à la liste des certificats de confiance du conteneur Forgejo. Il faut également que le conteneur forgejo ait accès à un réseau public.

(Action) Créer un répertoire forgejo à la racine du répertoire pour ce TP.

(Action) Copier dans le répertoire forgejo le certificat racine de Caddy.

(Action) Créer un fichier Containerfile ou Dockerfile qui étend l’image officielle de Forgejo pour y ajouter le certificat de Caddy

FROM codeberg.org/forgejo/forgejo:1.21.5-0
COPY caddy.crt /usr/local/share/ca-certificates/caddyca.crt
RUN update-ca-certificates

(Action) Déclarer la forge logicielle dans le fichier compose

Dans la section volumes du fichier compose, ajouter :

  forgejo_data:
    driver: local

Dans la section networks du fichier compose, ajouter :

  forgejo_public:
    name: forgejo_public
    internal: false

Dans la section services du fichier composer, ajouter :

  forgejo:
    build: forgejo/
    container_name: forgejo
    restart: always
    networks:
      - forgejo_public
      - caddy_backend
    volumes:
      forgejo_data:/data
    environment:
      USER_UID: 1000
      USER_GID: 1000
      FORGEJO__server__DOMAIN: git.tp-authn.broken-by-design.fr
      FORGEJO__server__ROOT_URL: 'https://%(DOMAIN)s:8443/'
      FORGEJO__server__DISABLE_SSH: true
      FORGEJO__database__DB_TYPE: sqlite3
      FORGEJO__admin__DEFAULT_EMAIL_NOTIFICATIONS: disabled
      # La ligne suivante est OK car nous utilisons un réseau "interne" et dédié
      FORGEJO__security__REVERSE_PROXY_TRUSTED_PROXIES: '*'
      FORGEJO__security__PASSWORD_HASH_ALGO: argon2
      FORGEJO__security__MIN_PASSWORD_LENGTH: 12
      FORGEJO__security__PASSWORD_COMPLEXITY: lower,upper,digit,spec
      FORGEJO__security__PASSWORD_CHECK_PWN: true
    depends_on:
      - caddy

(Action) Ajouter l’hôte virtuel pour forgejo dans Caddy à la fin du fichier caddy/Caddyfile

git.tp-authn.broken-by-design.fr:80 {
  redir https://git.tp-authn.broken-by-design.fr:8433{uri}
  log
}

git.tp-authn.broken-by-design.fr:443 {
  reverse_proxy forgejo:3000
  log
}

(Action) Détruire, reconstruire et recréer les conteneurs

podman compose -f podman-compose.yml down
podman compose -f podman-compose.yml build
podman compose -f podman-compose.yml up -d
docker compose -f docker-compose.yml down
docker compose -f docker-compose.yml build
docker compose -f docker-compose.yml up -d

(Action) Ouvrir https://git.tp-authn.broken-by-design.fr:8443/ dans son navigateur

(Action) Terminer la procédure d’installation de Forgejo en configurant un utilisateur administrateur et son mot de passe.

(Action) Ajouter une passkey pour le compte Administrateur du Forgejo

Pour ajouter la passkey, aller dans la “Configuration” du compte Administrateur de Forgejo, dans l’onglet “Sécurité”. Ensuite, dans la section “Clés de sécurité”, taper le nom “Bitwarden” et cliquer sur Ajouter une clé de sécurité. Dans la popup Bitwarden, sélectionner “Enregistrer la clé d’accès”.

(Action) Se déconnecter, et se reconnecter au compte administrateur, et utiliser la passkey comme second facteur d’authentification.

Installation d’un serveur Postgres pour le référentiel d’identités

Keycloak utilise une base de données pour stocker les identités entre autres informations.

Nous allons donc installer un serveur Postgres sous la forme d’un conteneur, et créer un réseau sur lequel Keycloak pourra le joindre.

(Action) Ajouter un serveur Postgres au fichier compose

Dans la section volumes, ajouter :

  psql_data:
    driver: local

Dans la section networks, ajouter :

  psql_backend:
    name: psql_backend
    internal: true

Dans la section services, ajouter :

  psql:
    image: docker.io/postgres:16.2-alpine
    container_name: psql
    restart: always
    volumes:
      - psql_data:/var/lib/postgresql/data
    networks:
      - psql_backend
    environment:
      PGDATA: /var/lib/postgresql/data/pgdata
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: keycloak

(Action) Lancer le conteneur Postgres

podman compose -f podman-compose.yml up psql

ou

docker compose -f docker-compose.yml up psql

Installation du référentiel d’identité

Le conteneur Keycloak est fourni sous la forme d’une image “non optimisée” et qui ne sait pas se servir de Postgresql comme base de données par défaut. Il est nécessaire “d’optimiser” l’image. En outre, Keycloak va avoir besoin de parler avec Forgejo ; il lui faut donc ajouter le certificat racine dans sa base de confiance. Contrairement à Forgejo, Keycloak n’utilise pas la base système et utilise à la place un keystore propre à Java.

(Action) Ajouter l’hôte idp.tp-authn.broken-by-design.fr dans le fichier hosts

10.108.0.1 idp.tp-authn.broken-by-design.fr

(Action) Ajouter l’hôte virtuel dans Caddy pour Keycloak

À la fin du fichier caddy/Caddyfile, ajouter :

idp.tp-authn.broken-by-design.fr:80 {
  redir https://idp.tp-authn.broken-by-design.fr:8433{uri}
  log
}

idp.tp-authn.broken-by-design.fr:443 {
  reverse_proxy keycloak:8080
  log
}

(Action) Créer un répertoire keycloak à la racine du répertoire du TP

(Action) Copier dans le répertoire keycloak le certificat de l’autorité de certification racine généré par Caddy

(Action) Créer un Containerfile (ou Dockerfile) dans le répertoire keycloak pour optimiser keycloak pour Postgresql

FROM quay.io/keycloak/keycloak:latest as builder
ENV KC_DB=postgres

COPY --chmod=0644 caddy.crt /opt/keycloak/conf/caddy.crt
RUN keytool -import -trustcacerts -keystore /opt/keycloak/conf/truststores/cacerts.jks -noprompt -storepass useless -file /opt/keycloak/conf/caddy.crt

WORKDIR /opt/keycloak
RUN /opt/keycloak/bin/kc.sh build

FROM quay.io/keycloak/keycloak:latest
COPY --from=builder /opt/keycloak/ /opt/keycloak/

(Action) Ajouter un conteneur keycloak dans le fichier compose

Dans la section networks, ajouter :

  keycloak_public:
    name: keycloak_public
    internal: false

Dans la section services, ajouter :

  keycloak:
    build: keycloak/
    container_name: keycloak
    restart: always
    networks:
      - keycloak_public
      - caddy_backend
      - psql_backend
    command: start
    environment:
      JAVA_OPTS: -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.err.encoding=UTF-8 -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -XX:+ExitOnOutOfMemoryError -Djava.security.egd=file:/dev/urandom -XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -XX:FlightRecorderOptions=stackdepth=512 -Xms64m -Xmx512m -Djavax.net.ssl.trustStore=/opt/keycloak/conf/truststores/cacerts.jks -Djavax.net.ssl.trustStorePassword=useless
      KC_PROXY_HEADERS: xforwarded
      KC_HTTP_ENABLED: "true"
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://psql/keycloak
      KC_DB_USER: keycloak
      KC_DB_PASSWORD: keycloak
      KC_HOSTNAME_URL: https://idp.tp-authn.broken-by-design.fr:8443/
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: Bonjour1!tititoto
    depends_on:
      - caddy
      - psql

(Action) Détruire, reconstruire et relancer les conteneurs

podman compose -f podman-compose.yml down
podman compose -f podman-compose.yml build
podman compose -f podman-compose.yml up -d
docker compose -f docker-compose.yml down
docker compose -f docker-compose.yml build
docker compose -f docker-compose.yml up -d

Configuration du référentiel d’identités

Dans cette section, nous allons configurer Keycloak afin qu’il dispose d’un domaine ("realm”) dédié au réseau d’exploitation (par opposition au réseau et au domaine d’administration). Dans ce domaine, nous allons créer un utilisateur. Nous allons également déclarer un “client” OpenID Connect pour Forgejo. Cette configuration client permettra à Forgejo de demander des informations à Keycloak concernant les utilisateurs.

(Action) Se connecter sur https://idp.tp-authn.broken-by-design.fr:8443/

Utiliser le compte “admin” et le mot de passe “Bonjour1!tititoto”

(Action) Créer un nouveau domaine (realm) nommé “TP” pour les comptes utilisateurs et utilisatrices d’exploitation

(Action) Collecter les informations pour l’ajout d’un nouveau client dans Forgejo.

Pour cela, se connecter au compte administrateur, sélectionner “Administration du site”, puis dans “Identité et accès”, sélectionner “Sources d’authentification”. Ajouter une source d’authentification. Sélectionner le type “OAuth2” et le fournisseur OAuth2 appelé “OpenID Connect”. Après avoir saisir le nom de l’authentification “keycloak”, récupérer l’adresse conseillée pour l’URL de rappel/redirection (https://git.tp-authn.broken-by-design.fr:8443/user/oauth2/keycloak/callback).

(Action) Sur Keycloak, créer un nouveau client OpenID Connect forgejo dans le domaine “TP”.

Dans le domaine “TP”, aller dans le menu “Clients”, et cliquer sur “Create client”. Appeler le client forgejo. Cliquer sur “Next”, puis sélectionner uniquement le flux d’authentification standard. Activer également l’authentification du client, puis cliquer sur “Next”. Indiquer l’URL racine https://git.tp-authn.broken-by-design.fr:8443/, l’URL de rappel valide https://git.tp-authn.broken-by-design.fr:8443/user/oauth2/keycloak/callback. Finalement, cliquer sur “Save”.

(Action) Récupérer le secret pour le client Forgejo.

Se rendre dans l’onglet “Credentials” du client forgejo et copier le client secret.

(Action) Configurer Forgejo pour utiliser Keycloak

Une fois “ajouter une source d’authentification” sélectionné, choisir le type “OAuth2”, et le fournisseur “OpenID Connect”. Indiquer le nom de fournisseur keycloak, l’ID du client forgejo, et le secret du client copié depuis Keycloak. Indiquer également l’URL de découverte https://idp.tp-authn.broken-by-design.fr:8443/realms/TP/.well-known/openid-configuration. Ajouter la source d’authentification.

(Action) Ajouter un utilisateur administrateur forgejo admin dans le domaine TP

Dans “Users”, sélectionner “Create new user”. Nommer l’utilisateur admin et créer l’utilisateur. Dans l’onglet “Credentials”, sélectionner “Set Password” et définir un mot de passe pour l’utilisateur admin. Utiliser Bitwarden/Vaultwarden pour suggérer un bon mot de passe, et le sauvegarder.

(Action) Associer l’utilisateur administrateur de Forgejo à l’identité admin du realm TP de Keycloak.

Dans Forgejo, aller dans “Configuration” du profil utilisateur. Dans l’onglet “Sécurité”, dans la section “Gérer les comptes liés”, sélectionner “Lier un compte”, et sélectionner “keycloak” dans la liste déroulante qui est apparue. S’authentifier au compte admin du realm TP. Compléter les champs demandés.

(Action) Tester l’authentification avec Keycloak sur Forgejo.

Se déconnecter du compte administrateur de Forgejo. Se reconnecter, en cliquant sur “Se connecter avec Keycloak”.

Gérer les groupes et les rôles de façon centralisée

Dans cette partie du TP, nous allons créer des rôles et des groupes et les assigner à nos utilisateurs admin et un nouvel utilisateur jean.dupont. Ces droits seront ajoutés dans Keycloak et dans Forgejo. Ensuite nous configurerons Forgejo pour qu’il utilise les informations récupérées grâce OpenID Connect pour ajouter l’utilisateur jean.dupont dans les bons groupes gérés par Forgejo.

(Action) Créer dans Forgejo une organisation “TP”

Cliquer sur le “+” en haut à droite, puis sélectionner “Nouvelle Organisation”. Appeler l’organisation “TP” et cliquer sur “Créer une organisation”.

(Action) Créer dans l’organisation Forgejo “TP”, une équipe mainteneur, avec des droits en écriture.

Une fois sur la page de l’organisation “TP”, sélectionner “Nouvelle équipe”. Nommer l’équipe mainteneur, et sélectionner le niveau de privilège “Ecriture” pour l’ensemble des actions possibles.

(Action) Créer dans l’organisation Forgejo “TP”, une équipe contributeur, avec des droits en lecture.

(Action) Ajouter au domaine TP de Keycloak des rôles mainteneur et contributeur.

Dans le domaine TP, sélectionner le menu “Realm Roles”, puis “Create role”. Nommer les rôles et cliquer sur “Save”.

(Action) Ajouter au domaine TP de Keycloak des groupes Mainteneurs du TP et Contributeurs du TP.

Dans le domaine TP, aller dans le menu “Groups”, puis cliquer sur “Create Group”. Nommer le groupe, puis cliquer sur “Create”. Renouveller l’opération pour le second groupe.

(Action) Assigner le rôle mainteneur aux membres du groupe Mainteneurs du TP et le rôle contributeur aux membres du groupe Contributeurs du TP.

Pour cela, dans le domaine TP, dans le menu “Groups”, cliquer sur un groupe, puis aller dans l’onglet “Role Mapping”, et cliquer sur “Assign Role”. Sélectionner le rôle approprié, puis cliquer sur “Assign”. Renouveler l’opération pour le second groupe.

(Action) Dans Keycloak, ajouter un utilisateur jean.dupont et lui définir un mot de passe avec Vaultwarden.

(Action) Ajouter jean.dupont au groupe Mainteneurs du TP.

Pour cela, dans le domaine TP, dans le menu “Groups”, cliquer sur le groupe Mainteneurs du TP, puis aller dans l’onglet “Members”. Cliquer sur “Add Member”, cocher l’utilisateur jean.dupont, puis cliquer sur “Add”.

(Action) Configurer l’exposition des rôles au client forgejo, afin de ne divulguer que la possession des rôles mainteneur et contributeur à ce client, sous une entrée forgejo-roles.

Dans le menu “Clients”, cliquer sur le client forgejo. Ensuite, dans l’onglet “Client Scopes”, cliquer sur l’entrée forgejo-dedicated. Cliquer sur le bouton “Add mapper”, puis “From predefined mappers”. Cocher “realm roles”, puis cliquer sur “Add”. Cliquer ensuite sur l’entrée “realm roles”, et remplacer la valeur dans “Token Claim Name” par forgejo-roles. Cocher également “Add to userinfo” et “Add to ID Token”, puis cliquer sur “Save”. Ensuite, revenir sur la fenêtre “forgejo-dedicated”, puis se rendre dans l’onglet “Scope”. Désactiver “Full scope allowed”, puis cliquer que “Assign Role”, cocher les deux rôles mainteneur et contributeur et cliquer sur “Assign”.

(Action) Configurer Forgejo pour tenir compte des rôles fournis par Keycloak.

Dans “Administration du site”, dans le menu “Identités et accès”, puis “Sources d’authentification”, cliquer sur “Editer” pour le fournisseur “keycloak”. Dans le champ “Associe les groupes réclamés avec les équipes de l’organisation. (Optionnel, nécessite un nom de réclamation)”, saisir le document JSON suivant :

{"mainteneur": {"TP": ["mainteneurs"]}, "contributeur": {"TP": ["contributeurs"]}}

Saisir également forgejo-roles dans le champ “Réclamer le nom fournissant les noms de groupe pour cette source. (facultatif)”, puis cliquer sur “Mettre à jour la source d’authentification”.

(Action) Configurer Forgejo pour qu’il retire les membres des équipes lors de la synchronisation avec Keycloak

Dans “Administration du site”, dans le menu “Identités et accès”, puis “Sources d’authentification”, cliquer sur “Editer” pour le fournisseur “keycloak”. Cocher la case “Supprimer les utilisateurs des équipes synchronisées si l’utilisateur n’appartient pas au groupe correspondant”, puis cliquer sur “Mettre à jour la source d’authentification”.

(Action) Constater qu’il n’y a aucun membre dans l’équipe mainteneurs sur Forgejo.

(Action) Se déconnecter de l’utilisateur admin du realm TP.

Se rendre à l’adresse https://idp.tp-authn.broken-by-design.fr:8443/realms/TP/account/ et se déconnecter.

(Action) Se connecter en tant que jean.dupont sur Forgejo, en se connectant avec Keycloak. Valider la création d’un nouveau compte. Constater que l’utilisateur appartient à l’équipe mainteneurs.

(Action, Optionnel) Dans Keycloak, retirer jean.dupont du groupe des utilisateurs “Mainteneurs du TP”.

(Action) Se déconnecter du compte jean.dupont sur Forgejo et sur Keycloak et se reconnecter. Constater que l’utilisateur n’est plus membre de l’équipe mainteneurs.

Configurer une authentification sans mot de passe

Dans cette partie du TP, nous allons améliorer la procédure d’authentification sur Keycloak en rendant l’usage du mot de passe optionnel, voire en l’éliminant totalement.

(Action) Créer une nouvelle procédure d’authentification à partir de celle préexistante “Browser”, ne permettant que l’authentification sans mot de passe avec une passkey, ou l’authentification avec mot de passe si l’attribut “password-authn-allowed” vaut “true”.

Dans le domaine TP, cliquer sur “Authentication”. Dans l’onglet “Flows”, sur le menu kebab au bout de la ligne “browser”, et sélectionner “Duplicate. Appeler ce flow “Browser TP”. Cliquer sur “Browser TP”, puis supprimer toutes les entrées en cliquant sur les icones poubelles.

Cliquer ensuite sur “Add execution”, sélectionner “Cookie” et cliquer sur “Add”. Dans la colonne “Requirement” de la ligne “Cookie”, sélectionner “Alternative”.

Cliquer ensuite sur “Add Subflow”, appeler ce flow “user active authentication”, et conserver le “flow type” à la valeur “Generic”. Dans la colonne “Requirement” de la ligne “user active authentication”, sélectionner “Alternative”.

Cliquer ensuite sur le “+” de la ligne “user active authentication”, puis sur “Add step”. Sélectionner “Username Form”.

Cliquer ensuite sur le “+” de la ligne “user active authentication”, puis sur “Add sub-flow”. Le nommer “required placeholder”, laisser le “flow type” à la valeur “Generic”, puis cliquer sur “Add”. Dans la colonne “Requirement” de la ligne “required placeholder”, sélectionner “Required”.

Cliquer ensuite sur le “+” de la ligne “required placeholder”, puis sur “Add sub-flow”. Le nommer “passwordless flow”, laisser le “flow type” à la valeur “Generic”, puis cliquer sur “Add”. Dans la colonne “Requirement” de la ligne “passwordless flow”, sélectionner “Alternative”.

Cliquer ensuite sur le “+” de la ligne “required placeholder”, puis sur “Add sub-flow”. Le nommer “passwordful flow”, laisser le “flow type” à la valeur “Generic”, puis cliquer sur “Add”. Dans la colonne “Requirement” de la ligne “passwordful flow”, sélectionner “Alternative”.

Cliquer ensuite sur le “+” de la ligne “passwordless flow”, puis sur “Add condition”. Sélectionner “Condition - user configured”, puis cliquer sur Add. Dans la colonne “Requirement” de la ligne “Condition - user configured” que l’on vient d’ajouter, sélectionner “Required”.

Cliquer ensuite sur le “+” de la ligne “passwordless flow”, puis sur “Add step”. Sélectionner “Webauthn Passwordless Authenticator”, puis cliquer sur Add. Dans la colonne “Requirement” de la ligne “Webauthn Passwordless Authenticator”, sélectionner “Required”.

Cliquer ensuite sur le “+” de la ligne “passwordful flow”, puis sur “Add subflow”. Le nommer “Conditional Flow - password allowed”, puis cliquer sur Add. Dans la colonne “Requirement” de la ligne “Conditional Flow - password allowed”, sélectionner “Conditional”.

Cliquer ensuite sur le “+” de la ligne “Conditional Flow - password allowed”, puis cliquer sur “Add condition”. Sélectionner “Condition - user attribute”, puis cliquer sur Add. Dans la colonne “Requirement” de la ligne “Condition - user attribute” que l’on vient d’ajouter, sélectionner “Required”. Cliquer ensuite sur la roue crantée de la ligne “Condition - user attribute”. Saisir dans le champ “Alias” la valeur “Authentification par mot de passe autorisée”, dans le champ “Attribute name” la valeur “password-authn-allowed”, dans le champ “Expected attribute value” la valeur “true”. Cocher “Include group attributes”, puis cliquer sur “Save”.

Cliquer ensuite sur le “+” de la ligne “Conditional Flow - password allowed”, puis sur “Add step”. Sélectionner “Password Form”, puis cliquer sur “Add”. Dans la colonne “Requirement”, sur la ligne “Password Form”, sélectionner “Required”.

(Action) Assigner cette procédure au flux d’authentification “Browser”

Dans la procédure “Browser TP”, cliquer sur “Action”, puis “Bind flow”. Sélectionner “Browser flow”, puis “Save”.

(Action) Ajouter un groupe Password allowed dans le domaine TP

Cliquer sur le menu “Groups”, puis “Create Group”. Nommer le groupe, puis cliquer sur “Create”.

(Action) Ajouter jean.dupont et admin en tant que membres du groupe Password allowed.

Cliquer sur le groupe Password allowed. Dans l’onglet “Members”, cliquer sur “Add member”. Cocher les deux utilisateurs et cliquer sur “Add”.

(Action) Ajouter un attribut password-authn-allowed au groupe Password allowed, avec la valeur true.

Cliquer sur le groupe Password allowed. Dans l’onglet “Attributes”, cliquer sur “Add attributes”. Saisir password-authn-allowed dans le champ “key” et true dans le champ “Value”. Cliquer sur “Save”.

(Action) Configurer l’enregistrement obligatoire d’une passkey pour admin

Dans le menu “Users” du domaine TP, cliquer sur l’utilisateur admin. Dans “Required actions”, sélectionner “Webauthn Register Passwordless”. Cliquer sur “Save”.

(Action) S’authentifier au compte admin avec un mot de passe. Enregister une passkey.

(Action) Se déconnecter et se reconnecter à l’utilisateur admin. Utiliser la passkey.

(Action) Retirer admin du groupe Password allowed

Dans le menu “Groups” du domaine TP, cliquer sur le groupe Password allowed, puis cocher l’utilisateur admin et cliquer sur “Leave”.

(Action) Retirer la passkey du compte admin

Dans le menu “Users” du domaine TP, cliquer sur l’utilisateur admin. Dans l’onglet “Credentials”, cliquer sur le menu kebab de la ligne Webauthn-password less, et cliquer “Delete” et confirmer.

(Action) Essayer de se connecter au compte admin et constater une erreur.

Panique morale face à l'écriture inclusive en capacité de droit

Celles et ceux qui me suivent savent certainement que j’ai récemment entrepris de reprendre mes études, grâce au temps libéré en devenant freelance. Je me suis donc inscrit en faculté de droit à l’Université de Paris 1, en capacité, ayant raté la fenêtre pour les inscriptions en L1. J’effectue cette capacité au CNED, afin de pouvoir bosser à mon rythme et au rythme de mes clients.

Parmi les différents outils de communication, un groupe de discussion non officiel existe sur Whatsapp, où des étudiants et étudiantes de capacité peuvent échanger entre elles et eux, et également avec certaines personnes plus expérimentées.

Usant couramment de l’écriture inclusive, j’ai donc tout naturellement utilisé ce sociolecte sur ces groupes de discussion, avant de me faire agresser verbalement par un participant sur mon emploi de cette forme d’écriture : “ça ne veut rien dire”, “les correcteurs ne vont pas te rater”, “blablabla”. S’en est suivi le message suivant, retranscrit en toute légalité puisque posté sur un groupe de discussion public (mais non listé) :

Bonsoir tout le monde, ici votre serviteur ! J’espère que vous allez bien. Un simple rappel si vous utilisez l’écriture inclusive et/ou le point médian. Son usage à l’Université est vivement déconseillé (dans vos copies mais également vos échanges avec l’administration (sic) qui n’a pas le droit d’utiliser ce langage d’ailleurs). Pour information la jurisprudence administrative a censuré une délibération de l’Université de Grenoble qui utilisait ce langage ET une proposition de loi vient d’être adoptée pour que soit mis un terme à son usage dans l’administration (sic) (actuellement une circulaire) ainsi que dans tout document officiel (contrat de travail, panneau, etc.). Aussi, vraiment, n’utilisez pas ce langage dans vos copies, au risque de vous faire saquer par votre correcteur et contrairement à ce que diront certains, vous ne ferez pas les social justice warriors en contestant vos résultats, ça ne marche pas comme ça. En fac de droit on parle français et on ne fait pas une copie militante avec un langage sorti des théories fumantes des woke (sic) des USA. C’est un simple conseil, libre à vous de vous faire votre propre expérience.

Je propose de ne pas relever outre mesure la stupidité d’assigner l’écriture inclusive aux “wokes des USA”.

D’une part, discréditer une information ou une pratique sur la base de son origine est une partie intégrante de la cancel culture. Or, selon ses détracteurs et détractrices, la cancel culture est l’apanage des Wokes ; preuve est donc faite que la cancel culture est également utilisée pour cancel les Wokes..! La belle ironie.

D’autre part, attribuer l’écriture inclusive à des anglophones, dont la langue est essentiellement non genrée relève du contreexploit intellectuel. La seule variation récente de la langue sur ce sujet est la généralisation, notamment dans les papiers scientifiques, de l’emploi de “she” (elle) ou du “they” singulier (dont l’usage prédate les “Wokes” de plusieurs siècles) pour désigner les personnes de genre inconnu ou non pertinent.

Je passerai également sur la stupidité de déclarer “qu’en fac de droit on parle français et on ne fait pas une copie militaire avec un langage [autre]”, puisque le français ne se décrète pas. Il s’agit d’une langue (et non d’un langage) vivante, appartenant à la francophonie, qui ne se limite pas à la France. Et c’est en cela qu’il est totalement inepte que des pouvoirs politiques tentent d’en imposer un usage. Je parle français ; juste pas le français de la République française, et ce faisant, je lutte contre l’emploi des langues comme outils d’oppression.

Dans cet article, je vais apporter, après une description un peu plus formelle de ce qu’est l’écriture inclusive, un éclairage sur les contrevérités énoncées dans l’extrait précédent.

Définition de l’écriture inclusive

L’objet de l’écriture inclusive en français est de faire apparaitre dans la langue les femmes, les personnes sur le spectre du genre, ou agenrée. En effet, le français, tel que reconnu par la République française, utilise le masculin comme genre neutre. La communauté scientifique a cependant pu établir que cette règle créée des biais dans les jugements et les interprétations qui sont en défaveur des personnes dont le genre est ainsi invisibilisé.

L’écriture inclusive recouvre un ensemble de pratiques typographiques, syntaxiques et grammaticales visant à faire apparaitre ces genres invisibilisés, à commencer par le genre féminin.

L’écriture inclusive est souvent réduite à l’usage du point médian (comme dans “les premier.ères arrivé.es seront les mieux servi.es”), aux pronoms neutres nouveaux (comme “iel” et “iels”), aux flexions neutres (comme “-ae”), ou à l’accord de proximité. Ses détracteurs et détractrices reprochent à l’emploi du point médian de rendre plus difficiles la lecture, et l’oralisation de ces écrits. Malgré mon emploi du point médian dans mes écrits, je conviens de la validité de ces reproches. J’argüerais cependant que le français est une langue qui a été artificiellement complexifiée en vue de séparer “les gens de lettres des ignorants et des simples femmes”. Ainsi, si nous voulions aller vers une langue plus simple à apprendre, comprendre et lire, peut-être devrions nous en premier lieu la rendre transparente, à l’instar de l’Espéranto.

Concernant l’emploi des pronoms neutres nouveaux (il en existe de plus anciens, utilisés de tous et toutes, et auxquels on ne pense même plus sous cet angle, nonobstant les divagations de Brigitte Macron et de Jean-Michel Blanquer), ces derniers sont dans plusieurs dictionnaires actuels. Le lecteur ou la lectrice aura donc tôt fait d’en saisir le sens, tout comme il ou elle aura certainement cherché le mot “sociolecte” employé plus haut dans ce texte, et qui fait partie du technolecte des sociolinguistes.

L’écriture inclusive ne se limite cependant pas à ces pratiques. Ainsi, la féminisation des noms communs, l’énumération, l’emploi de termes épicènes, et l’évitement de certaines antonomases sont autant de techniques d’écriture, bien moins remarquables et satisfaisant pourtant à l’inclusivité.

L’énumération consiste à lister les noms, dans leurs différentes formes genrées. On peut citer comme exemples, le célèbre “Françaises, Français…” au début des discours présidentiels, ou la locution “le Premier ou la Première Ministre”.

Les termes épicènes sont ceux dont la forme ne varie pas suivant que l’on se réfère à un nom féminin ou masculin, comme c’est le cas, par exemple avec le mot “ministre”.

Finalement, les antonomases comme “Homme”, comme dans “la Déclaration des Droits de l’Homme et du Citoyen”, peuvent être simplement évitées, lorsque cela est possible (i.e. quand on ne fait pas référence à un texte l’employant déjà…).

Comme nous le verrons dans l’analyse juridique qui suit, certaines de ces formes sont rejetées par la loi de la République française, tandis que d’autres sont requises.

Analyse juridique

Le message cité en début d’article évoque plusieurs éléments :

  • une décision du tribunal de Grenoble concernant un acte administratif ayant été annulé ;
  • une proposition de loi, adoptée par le Sénat fin octobre 2023 ;
  • une circulaire portant sur l’emploi de l’écriture inclusive dans l’Administration.

Dans ce chapitre, nous verrons que les conclusions énoncées par l’auteur du message cité sont trompeuses, fausses et incomplètes.

Décision du tribunal de Grenoble

La décision n°2005367 du tribunal administratif de Grenoble concerne une affaire dans laquelle un plaignant sollicite (notamment) l’annulation d’une délibération de l’Université de Grenoble rédigée en utilisant certaines techniques d’écriture inclusive, et notamment le point médian.

Le tribunal a condamné l’Université de Grenoble et ainsi annulé la délibération en cause. Les raisons avancées sont cependant particulièrement importantes. En effet, ses dernières ont rapport avec la nécessité de clarté et d’intelligibilité de la norme. Cette nécessité a valeur constitutionnelle. Elle a été commentée par Louis le Foyer de Costil, spécialiste en droit public, dans Marianne.

D’une part, cette décision ne saurait faire jurisprudence quant à la recevabilité d’une copie étudiante, puisque (fort heureusement) les copies étudiantes ne sont pas la norme, et n’ont pas besoin de s’élever au niveau de la qualité de la loi. En outre, des décisions passées ont débouté les plaignants ou plaignantes souhaitant faire censurer des écrits inclusifs dans d’autres contextes qu’un acte administratif. Ce fut notamment le cas le 14 mars 2023, où le tribunal administratif de Paris a rejeté un recours sur l’usage du point médian sur une plaque en hommage aux anciens présidents du conseil de Paris.

D’autre part, comme le note une étude sénatoriale : “le principe de clarté de la loi renvoie à l’exercice par le législateur de sa compétence, qu’il tient de l’article 34 de la Constitution. Le Conseil constitutionnel a consacré ce principe, avant de l’abandonner […], compte tenu des ambigüités fréquemment relevées dans la doctrine”.

S’agissant d’un jugement en première instance, et étant le premier à juger ainsi négativement de la clarté d’un acte administratif utilisant le point médian, cette décision est relativement fragile et “il faudrait que ce jugement soit confirmé par la cour administrative d’appel, par le Conseil d’État”, commente Louis le Foyer de Costil.

Proposition de loi sur l’écriture inclusive

Les termes employés par l’auteur du message cité en début d’article concernant la proposition de loi sur l’écriture inclusive sont trompeurs. En effet, en choisissant de dire que la proposition de loi a été adoptée, il entend ainsi faire croire que c’est le Parlement qui a adopté le texte, à l’issue de la procédure législative. Or, ce n’est pas le cas. Le texte a simplement été adopté en première lecture par le Sénat, le 31 octobre 2023. Cette adoption n’a rien de surprenant étant donné que la couleur politique du Sénat est à droite depuis sa création sous la Ve République, et que les paniques morales sur la désacralisation de la langue sont un marronnier des mouvements politiques de l’extrême centre, de la droite et de l’extrême droite. Le texte n’a cependant pas terminé son parcours législatif et doit encore être étudié par l’Assemblée Nationale qui aura le loisir de voter divers amendements, y compris des amendements de suppression. La responsabilité du gouvernement ne pourra également pas être engagée sur ce texte, compte tenu qu’il s’agit d’un texte social et que le Gouvernement a déjà utilisé le 49.3 lors la session parlementaire en cours.

Circulaire sur l’écriture inclusive

La circulaire du 17 novembre 2017 est “relative aux règles de féminisation et de rédaction des textes publiés au Journal officiel de la République française”. Ici encore, l’auteur du message cité trompe délibérément son auditoire en laissant entendre que sa portée s’applique également aux écrits des étudiants et des étudiantes. Pourtant le nom de la circulaire indique bien qu’elle se limite au Journal officiel.

Cette circulaire est cependant référencée dans le bulletin officiel de l’éducation nationale, de la jeunesse et des sports, du 5 mai 2021. Ce bulletin, rédigé par Jean-Michel Blanquer, encadre l’emploi de l’écriture inclusive dans les actes et usages administratifs, et dans le cadre de l’enseignement. Il convient néanmoins de noter que ce bulletin, fortement chargé politiquement, use du sophisme de la généralisation abusive, en tentant de pourfendre l’écriture inclusive d’une main, tout en en promouvant (inconsciemment) son usage d’une autre, dans un exercice d’équilibrisme absurde, démontrant la parfaite stupidité de son auteur.

En effet, ce bulletin “proscrit le recours à l’écriture dite inclusive, qui utilise notamment le point médian pour faire apparaitre simultanément les formes féminines et masculines d’un mot employé au masculin lorsque celui-ci est utilisé dans un sens générique”. Simultanément, le bulletin prescrit “de recourir à des formulations telles que le candidat ou la candidate afin de ne pas marquer de préférence de genre, ou à des formules telles que les inspecteurs et les inspectrices de l’éducation nationale pour rappeler la place des femmes dans toutes les fonctions”. Or cette formulation est une pratique d’écriture inclusive, nommée énumération, et qui a été présentée plus haut dans ce document.

En conséquence, non seulement l’écriture inclusive n’est pas proscrite sous toutes ses formes dans les copies des étudiants et des étudiantes de l’Université, mais elle n’est pas non plus proscrite, mais au contraire prescrite par le ministère par l’entremise de cette circulaire et de ce bulletin.

Pour résumer, si vous souhaitez être certain ou certaines, évitez le point médian, mais vous pouvez utiliser sans problème les termes épicènes, l’énumération, et l’évitement des antonomases discriminantes. Si vous utilisez le point médian et que vous êtes pénalisés ou pénalisées lors de la notation, un recours sera possible, car il n’existe pas de jurisprudence à ce sujet.

Finalement, faites attention aux arguments d’autorité, en particulier quand ces derniers sont donnés mâtinés d’idéologie.

Protocole d'autorisation par "double anonymat" pour la vérification d'âge respectueuse de la vie privée

Cet article effectue une analyse comparative du protocole d’autorisation d’accès à des sites requérant une vérification d’âge proposé par le LINC (Laboratoire d’innovation numérique de la CNIL), dit en “double anonymat”, et de Privacy Pass, un protocole similaire développé par l’IETF (Internet Engineering Task Force), avec la contribution de Google, Apple, Cloudflare, Fastly, LIC, Brave Software, The Tor Project, et Mozilla.

Ces deux protocoles sont génériques et n’ont rien de spécifique avec la vérification d’âge. Leur objectif est de prouver à un tiers qu’un utilisateur remplit un certain critère, sans révéler à ce tiers qui est exactement cet utilisateur parmi un groupe d’individus. Ainsi, bien que le protocole proposé par le LINC soit actuellement popularisé dans le cadre du contrôle d’accès aux sites pornographiques, celui-ci ne se limite pas à ce cas d’usage, et d’autres sont possibles (âge, genre, classe de niveaux de revenu, statut de vaccination, etc.) et certains sont même déjà évoqués sur le site du LINC1.

Par exemple, nous pouvons imaginer que cela fonctionne par grand palier selon la législation : 13 ans, 15 ans, 16 ans et 18 ans.

Dans cet article, nous verrons que le protocole proposé par le LINC comporte plusieurs risques pour la vie privée des utilisateurs. Parmi ces derniers, il est notamment question de fuite de données potentiellement caractérisables comme des données de santé, et de la capacité de l’État ou de son représentant à désanonymiser les utilisateurs en cas de collusion de plusieurs parties, de manière plus ou moins discrète.

Cet article est délibérément très technique et s’adresse à un public averti et formé à la cryptographie applicative et à la sécurité des protocoles réseau. La conclusion de l’article devrait être intelligible pour la plupart des lecteurs, mais elle n’aurait aucune valeur scientifique et technique sans le charabia qui la précède. Si vous êtes journaliste, faites-vous assister par un spécialiste de confiance pour vérifier mes dires avant de les reprendre.

Finalement, cet article ne couvre pas les nombreux moyens de contournement de la mesure technique souhaitée par le gouvernement français, au rang desquels les VPN. L’objectif est de discuter des mérites et des défauts des protocoles d’autorisation. Il est fait l’hypothèse (évidemment fantasque) que le monde entier a déployé cette méthode d’autorisation, ou qu’il n’est pas possible d’échapper à ce contrôle d’aucune manière que ce soit. Bienvenue en enfer.

Description informelle du protocole du LINC

Il n’existe à ce jour pas de papier scientifique ou whitepaper effectuant une description formelle du protocole proposé. Il existe cependant un article publié sur le site du LINC1, et le code source d’un démonstrateur2. Ces deux ressources sont assez anciennes, puisqu’elles ont été publiées en juin 2022, sans changement depuis lors. Si des évolutions ont eu lieu entre temps, ces dernières ne sont pas encore rendues publiques.

Description informelle des acteurs

Pour résumer ce qui est indiqué dans ces ressources, le protocole du LINC est composé de cinq acteurs ou types d’acteurs :

  • l’utilisateur et son navigateur (appelé Client dans le reste de cet article) ;
  • le site requérant une vérification de l’âge du Client (appelé Origin dans le reste de l’article, pour faciliter la comparaison avec Privacy Pass ;
  • des tiers certificateurs (appelés Issuers dans le reste de l’article) ;
  • l’Autorité qui maintient à jour une liste de tiers certificateurs de confiance ;
  • l’ouvreur (appelé Opener dans le reste de l’article) qui peut désanonymiser un tiers certificateur à partir d’une signature (nous reviendrons sur ce point).

Il convient de noter que l’Autorité et l’Opener sont des rôles pouvant être assumés par une unique entité ou des entités distinctes. Il est fait l’hypothèse que l’Autorité et l’Opener sont des entités étatiques ou mandatées par l’État français.

Si les termes Origin, Client, Issuer, Autorité et Opener semblent trop abstraits, il semble acceptable de les remplacer mentalement dans le reste de cet article de la façon suivante, tant qu’il reste clair à l’esprit de chacun qu’il ne s’agit que d’exemples :

  • Client : l’utilisateur ; vous, moi ;
  • Origin : un site pornographique, un débit de boisson, un site de jeu d’argent, un site de réservation pour un évènement (concerts, etc.) ;
  • Issuer : le site des Impôts ;
  • Autorité : l’État français ;
  • Opener : l’État français.

Description informelle des échanges

Pour consulter un site dont l’accès est restreint (noté Origin), celui-ci signale au Client, s’il n’est pas authentifié à l’aide d’un compte précédemment créé sur ce site, qu’une vérification du critère d’accès est requise. Pour cela, l’Origin fournit un “challenge”. Ce challenge consiste en des données permettant à l’Origin de vérifier, à l’issue des différents échanges, que l’autorisation d’accès délivrée a bien été émise pour cette Origin, et nul autre. Il contient également des métadonnées sur la nature du critère qui doit être contrôlé à propos du Client (p. ex. son âge supérieur à 18 ans).

Le Client télécharge ce challenge, puis se connecte auprès d’un Issuer et lui envoie le challenge. Cet Issuer vérifie si le Client remplit le critère. Par exemple, si c’est le site des Impôts, il vérifie à l’aide des informations d’état civil, si l’utilisateur est majeur. Dans le cas où le Client remplit le critère, l’Issuer effectue une opération cryptographique sur le challenge, et renvoie au Client le résultat de cette opération, que nous appellerons preuve.

Le Client télécharge cette preuve, puis il se connecte à nouveau à l’Origin et lui envoie la preuve délivrée par l’Issuer. L’Origin vérifie la validité de cette preuve grâce à des opérations cryptographiques, vérifie que cette preuve est bien fournie pour le challenge qu’il avait initialement fourni à l’utilisateur et vérifie que l’Issuer sélectionné par le Client a toujours la confiance de l’Autorité. Si ces conditions sont réunies, alors l’Origin accorde au Client l’accès à son service.

Description des propriétés de sécurité

Le protocole du LINC est affublé du qualificatif de “double anonymat”, car :

  • l’identité du Client, bien que connue et vérifiée par l’Issuer, reste inconnue de l’Origin. L’Origin n’a connaissance que du fait que l’utilisateur remplit ou non le critère d’admission.

  • l’identité de l’Issuer reste inconnue de l’Origin. En effet, les mécanismes cryptographiques mis en jeu par le protocole permettent à l’Origin de vérifier les preuves et de vérifier que l’Issuer a toujours la confiance de l’Autorité, sans jamais apprendre l’identité de l’Issuer.

Ce protocole vise donc à se défier des Origin, et à contrôler leur mécanisme d’autorisation d’accès en les privant de toute information d’identification.

En outre, ce protocole vise à ce que l’Issuer ne puisse discerner l’Origin que cherche à visiter le Client.

Protéger l’identité du Client semble une évidence, étant donné qu’avant la mise en place de ce type de contrôle d’accès, l’Origin ignorait tout de l’identité du Client.

Protéger l’identité de l’Issuer semble un peu plus surprenant. Cela permet néanmoins de limiter les risques que l’Origin apprenne des informations à propos de l’identité du Client. Par exemple, si le Client utilise le site des Impôts pour obtenir l’autorisation, alors l’Origin apprend que le Client est imposable (c’est-à-dire “potentiellement monétisable”) en plus de connaitre le critère de majorité.

Il convient de noter que la propriété d’anonymat de l’Issuer n’est pas limitée à l’Origin. En effet, avec les mécanismes cryptographiques mis en oeuvre par le protocole du LINC, il n’est possible pour personne d’autre que l’Opener de savoir qui est l’Issuer. Cela peut paraitre étonnant de prime abord, puisque l’utilisateur sait bien auprès de quel organisme il s’est authentifié pour obtenir la preuve de son admissibilité au critère. Il s’agit ici d’une subtilité du protocole : l’Issuer n’est en réalité pas un organisme unique, mais une clé cryptographique détenue par un organisme. Rien ne permet de garantir à qui que ce soit qu’un organisme ne possède qu’une et une seule clé cryptographique. Pour le dire plus simplement, un organisme peut incarner simultanément plusieurs Issuers à l’insu de tous·tes. Nous reviendrons sur ce point lors de l’analyse des défauts de ce protocole.

Description des mécanismes cryptographiques mis en oeuvre

Ces informations sont majoritairement tirées du démonstrateur publié sur Github2.

L’Origin génère un challenge comprenant deux informations publiques et une privée. Les informations publiques sont un nonce, et un critère d’âge. L’information privée est la durée de validité du challenge.

Le Client transmet le challenge tel quel à l’Issuer sans aucune modification.

L’Issuer effectue une “signature” grâce à la bibliothèque PBC 3, et génère ainsi une preuve à divulgation nulle de connaissance de sa connaissance de sa clé privée. Il convient de noter que cette clé privée fait partie d’un schéma de signature de groupe4, dont le domaine de confiance est géré par l’Autorité. La seule connaissance de la clé publique du groupe est suffisante pour vérifier la preuve émise par cette clé privée. Une fois cette preuve créée, elle est envoyée au Client.

Le Client transmet tel quel à l’Origin la preuve, sans aucune modification.

L’Origin vérifie que la preuve porte bien sur le challenge/nonce qu’il a initialement délivré à ce Client. Il vérifie que le challenge n’a pas expiré. Il vérifie ensuite la validité de la preuve, grâce à la clé publique de l’Autorité. Il vérifie également dans une liste de révocation fournie par l’Autorité que la clé privée ayant créé la preuve n’a pas été révoquée du domaine de confiance.

Description de Privacy Pass

Privacy Pass est un protocole d’autorisation d’accès préservant la vie privée du Client développé par l’IETF. Ce protocole a été initialement proposé, en 2017, par Cloudflare dans un objectif de limitation du nombre de CAPTCHA présentés aux Clients naviguant sur des sites protégés par Cloudflare depuis le réseau d’anonymisation Tor. Il a depuis été spécifié sous la forme d’Internet Drafts dont certains sont en passe d’être adoptés en tant que RFC. Il convient donc de noter que les éléments évoqués ci-dessous sont contemporains aux versions en cours d’études par le groupe de travail Privacy Pass WG5.

Privacy Pass est un protocole générique de vérification de critères, bien qu’à l’origine, le critère vérifié par Cloudflare soit limité à “l’intelligence humaine”, ou plus spécifique à la capacité à résoudre un puzzle sensément insoluble par les intelligences artificielles actuelles.

Il convient de noter qu’il existe deux méthodes de génération des jetons d’autorisation d’accès avec Privacy Pass. L’un repose sur un VOPRF (Verifiable Oblivious Pseudo Random Function)6 et des vérifications avec une clé privée. L’autre utilise des signatures et des vérifications à clé publique. Dans le cadre de cet article, nous n’étudierons que la version avec des signatures et vérifications à clé publique, afin de rester dans un cadre relativement comparable au protocole proposé par le LINC.

Description des acteurs de Privacy Pass

Privacy Pass est composé de quatre rôles :

  • Client : l’utilisateur et son navigateur ;
  • Origin : le site web requérant la vérification du critère ;
  • Attester : une entité en charge de vérifier le remplissage du critère par le Client ;
  • Issuer : l’entité délivrant la preuve admissible par l’Origin que le Client remplit un certain critère, selon l’Attester.

Il existe de nombreuses variations dans l’architecture de Privacy Pass. Chaque rôle peut être incarné par des entités distinctes ou plusieurs rôles peuvent l’être par une seule entité. Ainsi les combinaisons de rôle suivantes sont spécifiées :

  • Origin + Attester + Issuer ;
  • Origin + Issuer ;
  • Attester + Issuer.

Description des échanges

Lorsqu’un Client tente d’accéder à une Origin requérant un critère spécifique, l’Origin renvoie un ou plusieurs challenges.

Chaque challenge est composé des informations suivantes :

  • un type de jeton d’autorisation acceptable (en vue de permettre l’agilité cryptographique) ;

  • le nom d’un issuer pouvant répondre à ce challenge ;

  • optionnellement, une information de contexte, sous la forme d’une valeur opaque de 32 octets, permettant de restreindre le contexte d’utilisation du jeton d’autorisation ;

  • optionnellement, le nom de l’Origin ayant produit ce challenge.

L’information de contexte permet notamment de limiter l’utilisation à une Origin particulière, sans pour autant divulguer le nom de cette Origin auprès de l’Issuer. Elle permet aussi d’encoder des restrictions comme l’adresse IP depuis laquelle sera autorisée la présentation du jeton d’autorisation d’accès, ou encore la durée de validité du challenge.

Le Client choisit l’un des challenges, et contacte un Attester qui est en relation avec l’Issuer spécifié dans le challenge sélectionné. L’Attester vérifie si le critère est vérifié par le Client. Si tel est le cas, l’Attester témoignera de cela auprès de l’Issuer. Si l’Attester n’est pas Issuer, alors il faudra qu’une mise en relation des deux rôles soit effectuée, soit par l’intermédiaire du Client qui contactera l’Issuer avec une attestation signée par l’Attester, soit l’Attester contactera directement l’Issuer.

Que ce soit le Client ou l’Attester qui contacte l’Issuer, dans tous les cas, le Client devra fournir des informations additionnelles à destination de l’Issuer :

  • le type de jeton d’autorisation acceptable par l’Origin et par lui-même ;
  • l’identifiant tronqué à un octet de la biclé que l’Issuer doit utiliser pour générer le jeton d’autorisation ;
  • une valeur opaque.

L’identifiant de la biclé que l’Issuer doit utiliser pour générer le jeton d’autorisation est le condensat SHA-256 de la clé publique de l’Issuer au format SubjectPublicKeyInfo encodée en DER.

La valeur opaque est le résultat d’une opération de masquage (blinding) du condensat du challenge et d’un nonce généré par le Client, et de diverses autres métadonnées protocolaires.

Le Client peut envoyer plusieurs requêtes à l’Issuer (éventuellement par l’entremise de l’Attester) pour une même attestation, afin de pouvoir obtenir plusieurs jetons pour une seule vérification du critère. Pour chaque requête, il sera nécessaire que le nonce du client soit distinct.

L’Issuer vérifie qu’une attestation a été fournie par l’Attester et effectue la signature cryptographique de la valeur opaque qu’il a reçue.

Le Client récupère la/les valeur(s) signée(s) et la/les démasque (unblind).

Le Client envoie ensuite à l’Origin le nonce, la signature démasquée, le condensat du challenge et diverses métadonnées.

L’Origin vérifie alors la signature démasquée contre les différentes données reçues, grâce à la clé publique associée à l’Issuer que le Client a choisi. Si la signature est bonne, alors l’accès est accordé.

Description des propriétés de sécurité

Privacy Pass offre une protection raisonnable (mais pas absolue) sur l’identité du Client.

L’Origin ne peut connaitre l’identité du Client, et ce même en cas de collusion avec l’Issuer. Cela est assuré par le mécanisme de masquage, dont seul le Client connait la clé de démasquage et qui rend virtuellement impossible toute connexion entre la requête du Client à l’Issuer et celle qu’il effectue auprès de l’Origin.

L’Origin apprend néanmoins quel Issuer a émis la signature, et il peut donc inférer certaines informations sur l’Attester, si tous les Attesters ne sont pas accrédités par tous les Issuers. Et en connaissant/suspectant quel Attester a été utilisé, l’Origin peut alors inférer une information sur le Client. Pour reprendre l’exemple donné pour le protocole du LINC, si l’Attester est le service des Impôts, et que l’Issuer n’a comme seul Attester ce dernier, alors en sachant que cet Issuer a reçu une attestation du service des Impôts, alors il sait que le Client est imposable.

Privacy Pass prévoit également que le Client puisse vérifier la signature grâce à la clé publique de l’Issuer. Cette vérification de signature doit être couplée à la vérification en ligne7 que la clé publique qu’il connait et utilise pour vérifier la signature est la même que celle à laquelle tous les autres Clients qui auraient recours à cet Issuer, auraient utilisée. Ces deux vérifications permettent de contrecarrer une attaque dite par partitionnement de l’ensemble anonyme (anonymity set partitionning). En effet, sans elles, le Client pourrait avoir reçu une signature effectuée avec une clé privée qui ne serait utilisée par l’Issuer que pour certains Clients. Ces Clients ne feraient alors plus partie de l’ensemble anonyme global, vérifié par la clé publique commune, mais d’un sous-ensemble, distinguable, vérifiable uniquement par une clé publique distincte.

Finalement, la révocation d’un Issuer ne s’effectue pas de manière centralisée ; chaque Origin doit individuellement retirer de sa liste de confiance les Issuers qui ne sont plus de confiance.

Il existe de nombreuses variantes et options dans Privacy Pass, et il ne s’agit ici que d’un portrait partiel, afin des fins de comparaison avec le protocole du LINC. S’il devait il y avoir de nouveaux points de comparaison, à l’aune d’une mise à jour des spécifications de l’un ou l’autre des protocoles, ou si un oubli c’était glissé dans cet article, ce dernier sera mis à jour dès que l’auteur en aura connaissance.

Description des mécanismes cryptographiques mis en oeuvre

Privacy Pass est conçu de manière à être agile vis-à-vis de ses mécanismes cryptographiques. Cette agilité est encodée grâce aux indicateurs de types de jetons qui ont été évoqués à plusieurs reprises dans la section précédente.

À l’heure où cet article est écrit, il n’existe que deux types de jetons :

  • 0x0001 : VOPRF6 (P-384, SHA-384)
  • 0x0002 : Blind RSA8 (2048 bit)

Considérations de sécurité et de vie privée

Après cette longue présentation des deux protocoles, il est proposé de procéder à l’analyse comparative de ces derniers.

Lien entre identité du Client et session sur le site de l’Origin

Adresse IP de l’utilisateur

Le protocole du LINC est un protocole interactif. Pour chaque demande d’accès devant être autorisée, le Client doit contacter un Issuer afin d’obtenir une preuve que le critère est rempli.

À l’inverse, Privacy Pass est un protocole qui peut être interactif ou non. Il est ainsi possible de demander à un Issuer implémentant Privacy Pass de multiples jetons d’autorisation pour un même challenge. Le Client n’est alors pas obligé de rentrer en contact avec l’Attester ou l’Issuer à chaque accès à l’Origin, puisqu’il peut être en mesure de disposer de certains jetons “d’avance”.

La nécessité d’interactivité du protocole dit en “double anonymat” fait que le Client se présentera probablement à l’Issuer et à l’Origin avec la même adresse IP. Si cette dernière n’est pas partagée avec une large portion de l’ensemble anonyme (anonymity set), alors il sera possible de déduire l’identité de l’utilisateur et ses habitudes de consommation en cas de collusion de l’Issuer et de l’Origin.

Avec Privacy Pass en mode interactif, le problème est identique. Néanmoins, en mode non interactif, le Client a l’opportunité de moduler son adresse IP, par exemple grâce à un VPN, pour l’unique moment où il collectera l’ensemble de ses jetons d’autorisation.

Challenge

Avec le protocole du LINC, le challenge fourni par l’Origin est transmis verbatim à l’Issuer. En conséquence, la collusion des deux entités permet d’associer aisément l’identité d’un Client à une activité sur une session sur l’Origin par simple journalisation puis comparaison des challenges émis et reçus.

En outre, même sans collusion, il est possible que l’Issuer puisse découvrir quelle Origin est visitée par le Client grâce à des motifs discernables dans le challenge. Par exemple, dans le démonstrateur développé par le LINC, le challenge est simplement la date du serveur. Il suffira alors de quelques requêtes par quelques Clients pour réussir à discerner l’heure exacte d’un serveur après gommage statistique des latences des requêtes. L’Issuer sera ensuite capable d’identifier de manière totalement passive quel site est consulté par quel Client sur la simple base de l’information volontairement transmise par le Client. Même si le challenge était un nombre aléatoire, il pourrait être possible d’identifier l’Origin si l’Origin n’utilise pas un générateur de nombres aléatoires cryptographiquement sûr. En effet, après un certain nombre de requêtes, il serait possible de retrouver l’état du générateur de nombres aléatoires, et d’ainsi distinguer plusieurs Origin de manière passive9.

Avec Privacy Pass, comme le challenge émis par l’Origin est intégré dans une requête dont le contenu est masqué avant d’être envoyée à l’Issuer, il n’est pas possible de faire ce lien, même en cas de collusion de l’Origin et de l’Issuer. Le Client participe activement au protocole et se défie de l’Origin et de l’Issuer.

Preuve

Avec le protocole du LINC, la preuve à divulgation nulle de connaissance émise par l’Issuer est transmise verbatim à l’Origin. Comme pour le challenge dans la section précédente, il est possible de relier l’identité du Client à une session sur l’Origin grâce à cette valeur commune.

Avec Privacy Pass, comme dans la section précédente, le démasquage de la réponse de l’Issuer permet de créer une rupture entre les données connues de l’Issuer et celles connues de l’Origin. En conséquence, il n’est pas possible de relier l’identité du Client à une session sur l’Origin par ce biais. Le client participe activement au protocole et se défie de l’Origin et de l’Issuer.

Partitionnement de l’ensemble anonyme

Le protocole du LINC prévoit que seul l’Opener puisse être en mesure de recouvrer quel Issuer a émis une preuve donnée. Cette propriété est délibérée et constitue l’un des deux “anonymats” de ce protocole dit à “double anonymat”. Elle est implémentée grâce à l’emploi d’un mécanisme de signature cryptographique de groupe4, et elle vise notamment à ce que l’Origin ne puisse distinguer l’Issuer.

Hélas, l’Origin n’est pas le seul acteur à ne pouvoir distinguer quel Issuer et plus précisément quelle clé privée d’Issuer a été utilisée pour générer une preuve. Les Clients ne le peuvent pas non plus.

Ainsi, dans le cas où l’Autorité et l’Issuer sont malveillants, il est possible qu’un sous-ensemble de l’ensemble anonyme (c.-à-d. certains Clients) voie ses preuves générées à l’aide de clés privées spécifiques. Ces preuves sont indistinguables de toutes les autres preuves, sauf pour l’Opener qui sera en mesure de singulariser les Clients ayant présenté à l’Origin ces preuves. Il ne semble pas improbable, dans le cas où l’Opener est l’État français qu’une perquisition des serveurs de l’Origin permet d’obtenir des journaux applicatifs contenant ces preuves et leur association à des sessions de navigation.

Privacy Pass, de son côté, peut également mettre en oeuvre des attaques par partitionnement de l’ensemble anonyme, notamment en incitant le Client à recourir à plusieurs Issuers tour à tour. Dans le cas où le Client se conforme à ces incitations, il est alors possible de partitionner l’ensemble anonyme en plaçant le Client à l’intersection des ensembles des Clients étant capables ou incapables d’obtenir des jetons d’autorisation de certains Issuers. Par exemple, un Client pourrait être identifié dans un sous-ensemble anonyme dont ses membres peuvent prouver leur âge grâce à leur banque, et à la sécurité sociale, et jamais via le service des Impôts. Ce sous-ensemble pourrait ainsi correspondre aux jeunes encore rattachés au foyer fiscal de leurs parents/tuteurs. Bien que Privacy Pass spécifie un moyen d’inciter les Clients à fournir des jetons d’autorisation provenant de tel ou tel Issuer plutôt que d’autres, le choix revient au logiciel mis en oeuvre par l’utilisateur de suivre cette incitation ou non. Il est donc possible que certains logiciels implémentant Privacy Pass ne soient pas concernés par cette attaque.

Futilité de l’interactivité comme méthode de prévention du marché noir

Le protocole du LINC doit et Privacy Pass peut fonctionner en mode interactif. Ce mode de fonctionnement oblige le Client à obtenir une preuve ou un jeton d’autorisation frais auprès d’un Issuer à chaque accès à une Origin. Cette obligation signifie que les Issuers seront en mesure de reconnaitre des habitudes de consommation. Dans le cas des sites pornographiques, ce mode est dangereux puisqu’une personne ayant développé une addiction à la pornographie pourra être identifiée comme telle par un Issuer qui lui remettrait des dizaines des preuves/jetons d’autorisation par jour. Or, les addictions sont des informations de santé10 11.

Un des risques perçus par le mode non interactif où le Client est autorisé à demander plusieurs preuves/jetons d’autorisation pour un unique challenge est la création d’un marché noir des preuves/jetons d’autorisation. En effet, un Client disposant de plusieurs jetons pourrait distribuer ou vendre ces derniers sans que l’Origin ne puisse distinguer si la preuve a bien été produite pour le Client qui la lui présente.

D’une part, il existe une contremesure assez simple, et qui est même recommandée dans la spécification de Privacy Pass : relier le challenge à un attribut de la connexion entre le Client et l’Origin : l’adresse IP du Client ou l’identifiant de la connexion TLS 12 (Transport Layer Security).

Une telle contremesure permettrait de passer en mode non interactif, tout en contrecarrant l’essentiel des risques liés au marché noir.

Ensuite, en admettant que la preuve/le jeton d’autorisation soit bien présenté à l’Origin par le Client l’ayant demandé à l’Issuer, la première chose que fera l’Origin après vérification de l’authenticité de ce jeton sera de créer une session HTTP classique dans laquelle il sera noté que ce Client est autorisé à accéder au contenu du site. La plupart des sites n’appliquent pas de politique de sécurité associant fortement une session HTTP à une session TLS. Un utilisateur malveillant pourra alors revendre non pas son jeton d’autorisation, mais son identifiant de session HTTP. Cette technique de partage de sessions HTTP par la divulgation de l’identifiant de session est bien connu des auditeurs en sécurité, qui la connaisse sous le nom de Session Fixation13. Un tel partage pourrait même être aisément automatisé à grande échelle grâce à une extension navigateur.

L’usage du mode interactif pour le protocole proposé par le LINC dans le cadre de la demande de restriction d’accès aux sites pornographiques par le gouvernement français n’est donc pas pertinent, et constitue au mieux un choix mal informé.

Problème de la double dépense

Le problème de double dépense survient lorsqu’une même preuve ou un même jeton d’autorisation peut être utilisé sur plusieurs Origin, alors que le protocole souhaiterait voir la preuve/le jeton consommé et ne plus être réutilisable.

Il existe de nombreuses manières de résoudre ce problème de double dépense. L’une d’entre elles, la plus simple théoriquement et bien souvent la plus difficile à implémenter dans la pratique, est la conception d’un registre commun à l’ensemble des Origin. Ces dernières inscriraient au registre commun les jetons qu’elles auraient dépensés, après avoir consulté ce dernier pour s’assurer qu’une autre Origin ne l’aurait déjà fait. Hélas, cette méthode peut présenter des problèmes de vie privée, puisque ce registre devrait être largement consultable et contiendrait indirectement des statistiques de fréquentation des sites (une donnée métier sensible).

Une autre méthode tient dans la génération de challenges aléatoires, dont la probabilité de collision avec un autre challenge émis par une autre Origin serait négligeable. C’est la méthode recommandée par la spécification de Privacy Pass.

De manière étrange, dans le démonstrateur proposé par le LINC, le challenge n’est pas aléatoire, mais est la sortie d’une fonction monotonique : l’heure du serveur. L’heure ne peut constituer un bon moyen de prévention contre la double dépense, puisque le même challenge peut être alors émis par plusieurs Origin. Il s’agit surement d’une facilité utilisée par le développeur dans le cadre du démonstrateur. Dans une implémentation réelle, il serait néanmoins nécessaire de prévenir la double dépense à l’aide d’un challenge généré à l’aide d’un générateur de nombres aléatoires cryptographiquement sûr.

Conclusion

Dans cet article, nous avons étudié deux technologies pouvant être utilisées à des fins de contrôle d’accès en fonction de critères vérifiés par des tiers. Ces technologies prétendent pouvoir prouver à un tiers qu’un utilisateur remplit ces critères sans dévoiler d’informations personnelles. Les critères peuvent être variés, et sont extensibles : seuil d’âge, genre, niveaux de revenu, statut de santé, etc.

Privacy Pass, un protocole spécifié par un consortium informel sous l’égide de l’IETF, remplit cet objectif, sur le papier et dans certaines conditions d’implémentation.

De son côté, le protocole du LINC dit en “double anonymat” échoue maintes fois à protéger l’identité de l’individu, par erreur de conception ou sous la contrainte du cahier des charges du commanditaire. Cela est dû notamment à l’interactivité du protocole, et au rôle passif de l’utilisateur lors des échanges.

L’interactivité permet de relier nominativement l’utilisateur à une session sur le site qu’il souhaite consulter, et fuite ses fréquences de consultation, ce qui peut être une donnée de santé dans le cas d’une addiction (au jeu, à la pornographie, à l’alcool, etc.). Outre son aspect nocif pour la vie privée de l’utilisateur, cette interactivité se révèle totalement futile dans le combat contre l’émergence de marchés noirs de jetons d’autorisation permettant l’accès à des personnes ne remplissant pas les critères.

La passivité de l’utilisateur dans les échanges, quant à elle, permet de relier également l’identité de l’utilisateur à sa session sur le site visité, et donc potentiellement à ses préférences sexuelles. Elle permet également aux organismes attestant que l’utilisateur remplit un critère donné d’apprendre, dans certaines conditions, quel site est visité.

Finalement, il peut être intéressant de noter que le démonstrateur du LINC comporte une fragilité d’implémentation qui permet d’utiliser les jetons d’autorisations d’accès plusieurs fois, sur plusieurs sites distincts.

Secure Boot: this is not the protection we are looking for

Before being a technology, secure boot is an English expression. But, what does it mean to boot securely? What are we trying to achieve and to protect?

First, we want user data protection from a secure system. User data is really any piece of data a user might display or edit, including the obvious office documents but also configuration files, databases, downloaded files, log files, user commands, etc. These data must be protected in confidentiality. For this, the system must ensure that these data are protected at rest, when the system is shutdown. However, this is not enough, since access from unauthorized software could leak the data, for instance over the network or by copying it on an external removable drive. Hence, a secure system should only run authorized software.

Secure Boot, the technology

Secure Boot1 helps running only authorized software on a machine. It does so by bootstrapping the security of the system at boot time, by verifying signatures on various software components, before giving way to other technologies to keep the system secure, later on. Secure boot works by authorizing only select executables to be run. Authorized executables are signed using public cryptography, and the keys used to verify those signatures are stored securely in UEFI “databases”.

UEFI is the successor of the now nearly defunct BIOS. It is an interface between the operating system and the manufacturer platform, a firmware, that runs very early on most modern systems. The platform is responsible of, among many other things, executing the bootloader (for instance, Grub or systemd-boot). Generally, the bootloader then starts an operating system. In the case of GNU/Systemd/Linux, the bootloader runs the Linux kernel, which shuts down all UEFI Boot Services, before dropping its privileges and then doing “Linux stuff” (like starting the userland part of the operating system). This privilege drop is very important because this is what ensures that no code past that point is able to tamper with the UEFI sensitive information, including UEFI variables, which contains sensitive data.

Secure Boot aims at securing “everything” that is executed prior to that privilege drop. Once the privileges are drop, Secure Boot is done and it is up to the operating system to extend that integrity/authenticity protection and ensure that only authorized software is run. Most Linux distros do not even try to do it, although there are notable exceptions (Chrome OS, Android, just to name a few). If we run one of the distros that do not leverage technologies such as dm-verity, fs-verity (with signatures) or Linux IMA (Integrity Management Architecture), then Secure Boot protected the boot integrity for basically nothing, because the security chain is broken by the operating system, and user data is at risk from possibly any tainted userland executable.

Nevertheless, ANSSI, the French Infosec Agency recommends in its GNU/Linux security guide2 to enable Secure Boot (R3) for all systems requiring medium security level (level 2 out of 4, 1 being the minimal requirements and 4 being for highly secure systems). Meanwhile, using a Unified Kernel Image to bundle the Linux kernel with a initramfs into a single executable that can be verified by Secure Boot is only recommended3 for highly secure systems (security level 4 out of 4). No recommendation is ever done about using one of the aforementioned integrity features to ensure operating system integrity.

Interestingly, a poll on the fediverse4 revealed that 89% of the respondants thinks that Secure Boot is necessary for intermediate security level, and 45% even think that it should be a minimum requirement.

This article is a strong push-back against ANSSI “recommendation”, and it attempts to prove that it is not only useless but incoherent and misleading.

Signature verification and default public keys

Secure boot relies on a set of public keys to verify authorized software authenticity. By default, most vendors ship Microsoft public keys. These keys sign all Microsoft Windows version, of course, but to avoid a monopoly, other executables were signed. The list is ought to be short (and unfortunately, it is not) because with each signature and authorized software, the attack surface grows and the probability of a vulnerability raises. Several were already found in the recent past (e.g. CVE-2020-107135, CVE-2022-343016, CVE-2022-343027, CVE-2022-343038.

For this reason, many software still require that Secure Boot be deactivated, including firmware updates by some manufacturers, including Intel9 or Lenovo10.

Grub and the kernels are not directly authorized by Microsoft, as it would require for each and every single version to be signed individually by Microsoft. Instead, a binary called Shim11 was developed. This rather small, auditable, innocuous-looking program is signed by Microsoft. Its role is basically that of a trojan horse (or a security pivot, depending on the way you look at it). Indeed, its only purpose is to cryptographically verify the authenticity of any executable. However, this time, the list of public keys used to verify these executables is not directly under the control of Microsoft. These public keys are either built in Shim itself or stored in a EFI variable serving as a database.

Additionally, shim public key list can be altered by any user able to prove “presence”. This is the case of any user using the local console or using a BMC for remote console access. Once a new public key enrolled, all executables verifiable by that key can run in the UEFI privileged mode, and for instance create a persistent backdoor in the bootstrapping code of the machine.

If this was not enough, users able to prove “presence” can also disable shim verification of authorized software. This means that shim can be used to have the system believe that Secure Boot was used to bootstrap security, while untrusted code was ultimately run within the UEFI privileged mode.

Hence, thanks to shim, just about any signed or unsigned, trusted or untrusted executable can be validated by proxy by Secure Boot using Microsoft keys.

And this conclusion signs (pun intended) the second incoherence in ANSSI recommendations. Indeed, they recommend replacing Microsoft keys by our own set of keys only for highly secure security level (R4). This means that all systems with intermediate (2/4) and enhanced (3/4) security levels will have the false sense of security of running Secure Boot while exposing themselves to attackers capable of proving “presence”.

For what it is worth, shim does provide a way secure all operations, including disabling verification or enrolling new keys by setting a password on shim’s MOK (Machine Owner Keys) Manager12. However, the feature is mostly unused because no Linux distro enables it (it requires user interaction during the boot procedure) and system administrators often mistake the MOK Manager password, which secures the access to the MOK Manager as a whole, with the password that is asked when running mokutil commands (including --import and --disable-verification), which is just a password used to confirm the will of the user in the MOK Manager. The author of this article failed to find a single tutorial or article discussing the necessity of setting a MOK Manager for a Secure Boot. ANSSI also failed to recommend that. Most interestingly, setting a MOK Manager password is recommended even for Windows administrators that have no intention of ever running Linux, because MOK Manager is one of the executables signed by the Microsoft keys.

Using our own set of Secure Boot keys (PK and KEK)

Since replacing the Microsoft keys and signing only authorized binaries (i.e. our own Unified Kernel Image and specifically not shim) is an option, why not do that? It just requires that the system administrators replace the Secure Boot keys. The question of who controls the private key and the signature process is entirely up to whether the signed executable is altered or not by the system administrator. Distros could ship public keys and Unified Kernel Images instead of relying on Shim. If we run our own kernel, as recommended by ANSSI (R15 to R27), then we need to handle the private key and the signature process ourselves.

With a Unified Kernel Image signed and verified by Secure Boot, we are now sure that only our code is executed, and we can safely ask for the user passphrase to unlock the LUKS container. This way, user data is protected at rest, and accessed only by authorized software. Cool. Except it is not.

First, as mentioned earlier, the only thing that we verified is that the Unified Kernel Image is authentic. That kernel must then verify the operating system to ensure that only verified software is run. This requires cryptographic verification of every single executable (binaries, scripts and executable configuration files (because… yeah… that’s a thing)) in the operating system. This can be achieved if we, at minimum, do the following:

  • use a read-only filesystem for our partitions containing executable code, enforced using dm-verity, or fs-verity with file signature or Linux IMA, or something similar;

  • use the noexec mount option on data partitions;

  • modify command interpreters to ensure they do not execute scripts from mount points with noexec nor from STDIN;

  • prevent writable memory from ever getting executed, by patching the kernel;

  • ensure that no executable configuration file is writable.

Welcome in Wonderland, Alice.

Ok, but let’s assume we go down that rabbit hole… are we secure yet?

No. No, we are not.

Because there is no way of telling if our Secure Boot implementation is tainted by an attacker or not. Indeed, someone could have flashed our UEFI firmware before we enabled Secure Boot. Or they could have disabled it, flashed and reenabled it, if we did not have a UEFI administration password at some point. Or they could have abused shim when we were using the default keys to flash UEFI.

Nirvana Fallacy much?

So, if Secure Boot is not a good answer, especially against an attacker capable of physical access or remote access through a BMC, what is? Is there a better solution?

Well, to the best of the author knowledge, there is one: using a TPM. Using a TPM will not necessarily prevent an attacker from tainting the firmware. It will not necessarily prevent booting untrusted and unverified executables. What a TPM can give us is the ability to unseal a LUKS passphrase and get access to user data if and only if the cryptographically verified right version of UEFI firmware, Unified Kernel Image and operating system image have been booted. This is based on PCR policies, that ties the sealed passphrase to a particular signed set of executables measured by the TPM. This approach ensures user data confidentiality at rest and the authenticity of the executables that are on disk during boot. Of course, it does not prevent executing writable memory, nor user data flagged as executable, including scripts, and executable configuration files. And of course, having a signed set of policies means handling a private key and designing a signature procedure. There is no free lunch. Sorry.

However, using a TPM offers the same security level against attackers with physical access, attackers with remote access through a BMC, and a rogue system administrator, and that is something that Secure Boot cannot brag about.

Conclusion

So there you have it: recommending idly Secure Boot for all systems requiring intermediate security level accomplishes nothing, except maybe giving more work to system administrators that are recompiling their kernel, while offering exactly no measurable security against many threats if UEFI Administrative password and MOK Manager passwords are not set. This is especially true for laptop systems where physical access cannot be prevented for obvious reasons. For servers in colocation, the risk of physical access is not null. And finally for many servers, the risk of a rogue employee somewhere in the supply chain, or the maintenance chain cannot be easily ruled out.


  1. UEFI Specifications ↩︎

  2. no English version yet. French version ↩︎

  3. “Protect command line parameters and initramfs with Secure Boot” ↩︎

  4. Mastodon poll ↩︎

  5. CVE-2020-10713 ↩︎

  6. CVE-2022-34301 ↩︎

  7. CVE-2022-34302 ↩︎

  8. CVE-2022-34303 ↩︎

  9. Intel UEFI Flash BIOS Update Instruction ↩︎

  10. Lenovo flash BIOS with UEFI tool ↩︎

  11. shim source code ↩︎

  12. The command is mokutil --password. Please consider using it. ↩︎

Building a Debian rootfs from an unprivileged user with debootstrap

At Gatewatcher1, we put efforts in making our building system reproducible and working offline, so that we can reduce the risk of supply chain attacks. Some efforts are also made so that our building system run with as few privileges as possible.

One of the few things we were still running as a privileged user recently was the build of our initial Debian root filesystem, for our base system and for our containers.

Indeed, the official Debian Docker container from Docker Hub was not generated in a way that we can consider secure for our need. It basically downloads a blob from a web server, does no verification whatsoever of that blob and ships it as the root filesystem2. Even though the root filesystem they are using can be rebuilt in a reproducible way, downloading the result from Internet without verifying it against the expected hash is sort of missing the point of reproducible builds. Also, debuerreotype uses debootstrap, which is problematic in itself, as explained hereafter.

To create such root filesystem, multiple tools are provided by the Debian team, among which debootstrap, and multistrap.

Multistrap has not been updated in many years3, and suffers from some limitations that were show-stoppers for us, but it is capable to create root filesystems from an unprivileged user without hacks.

On the other hand, deboostrap is not really friendly with the idea of building a system from an unprivileged user.

First of, there is a check to ensure we are running it with UID 04. This can be bypassed in several documented ways, including using fakeroot, which overloads some libc calls, using LD_PRELOAD. An other, less hacky, way is to run the program in a user namespace.

Unfortunately, this is not sufficient to run debootstrap, since it performs another check consisting of trying to create a “/dev/null” node5. This is more problematic since nodes cannot be created from a user namespace, as this would create a easy way of escaping the namespace.

As it seems, though, there is a way to build an unprivileged Debian root filesystem that is even built into deboostrap, using the installation variant “fakechroot”. Alternate code paths exist in deboostrap when this variant is selected that side-step some checks, and fake some calls. This variant also adds a check to ensure this variant is run only if the fakechroot utility is in use. Therefore, you are expected to run debootstap as followed, as documented in the fakechroot manpage:

# apt update && apt install -y debootstrap fakeroot fakechroot
$ fakechroot fakeroot debootstrap --variant=fakechroot bullseye $HOME/rootfs

fakechroot works by overloading some functions with LD_PRELOAD, and has some documented limitations regarding symlinks. As it happens, these limitations include rewrites of absolute symlinks, by prefixing them with the path of the faked chroot. As a result, within the chroot, you will find links that are broken when actually chrooting, such as when you would use that directory hierarchy as a root filesystem on a container or a virtual machine.

With fakechroot:

$ readlink /path/to/my/chroot/usr/sbin/telinit
/path/to/my/chroot/bin/systemctl

Without fakechroot (this is what you want to see, in a normal system):

$ readlink /path/to/my/chroot/usr/sbin/telinit
/bin/systemctl

After some verifications, we decided that it was safe to fake the use of fakechroot, while using the “fakechroot” installation variant. For this, we set the environment variable FAKECHROOT to true, which fakechroot is supposed to set and which is controlled by debootstrap to authorize the use of the “fakechroot” variant. And it worked.

So to build a working root filesystem from an unprivileged user, we are now doing the following:

$ podman unshare
# FAKECHROOT=true debootstrap --variant=fakechroot bullseye chroot/
# tar -C chroot/ --exclude=dev/* -czf ./chroot.tgz .
# exit
$ cat <<EOF > Containerfile
FROM scratch
ADD chroot.tgz .
CMD ["/bin/bash"]
EOF
$ podman build -f Containerfile

This series of commands builds a Debian container from an unprivileged user. More work needs to be done to achieve offline reproducible builds, of course, but none require hacks like this, thankfully.

Secure large file decryption using Linux, Go and Nacl

It is often said that you should not create your own cryptographic algorithm. That in cryptography, one should not be original; that security is in the beaten track, and in particular those which are beaten by cryptographers. What is often forgotten is that using cryptography is also perilous. In this article we will discuss how to handle large files when decrypting, and how to do it securely on a Linux system. This article will be illustrated with Go code.

What is a “large file” and how is it different from “small files”?

A small file is anything that is reasonable to store in RAM, in its entirety. RAM is a sanctuary where a program can store information. This sanctuary offers a relatively strong guarantee of isolation from interference from other tasks/processes.

Conversely, a large file is a file that cannot be stored in its entirety in the RAM of the system that will decrypt it. Therefore, interferences can occur. They are of several kinds.

Modifications of the encrypted data

The first problem is that the encrypted data can be modified by an external program. Indeed, as soon as a file is visible in a directory, any program running with the necessary privileges can open a file descriptor on this file. Thanks to this file descriptor, it is then possible to modify the file. This includes programs running with the same privileges as the program that is in charge of decryption.

This is not necessarily a problem. The state of the art in applied cryptography is to use authenticated encryption. This type of encryption verifies the cryptographic integrity of the encrypted content, while it decrypts it. Some algorithms and encryption modes, such as AES-GCM or AES-OCB, perform these operations in “one pass”. That is to say that the authenticity check is performed as the decryption proceeds and an authenticity verdict is given at the end of the decryption. Alas, not all encryption modes are one-pass; thus AES-CCM, for example, will first perform an integrity check (first pass) and then perform decryption (second pass) if the data was initially determined to be authentic. Unfortunately, an attacker is then able to alter the data between the first and second pass. The decrypted content is then declared authentic, even though it has been altered.

Consequently, when a one-pass decryption mode is not used, it is necessary to use a private copy of the data, either in RAM or by other system tricks that will be detailed later in this document.

Early use of data whose authenticity is not yet assured

When the decrypted data is too large to be stored in RAM, it is necessary to write it to disk. Unfortunately, security problems can occur during this passage on disk.

The list of these problems cannot be exhaustive, but it is possible to think of certain software that would use the data in an anticipated way. Indeed, some software monitors the contents of a folder with inotify(7) ; this is the case of most file explorers.

This early reading can result in incorrect interpretations of the file. This is not the most unfortunate consequence, however. In the case of one-pass encryption, or integrity checking after decryption (see the OpenPGP section of this document), it is possible that the file containing the decrypted file contains malicious code added by an attacker who corrupted the encrypted document. It is indeed very important to understand that encryption alone is not sufficient to guarantee the integrity of a data, and that it is necessary to use an integrity pattern, and to verify it, before using the decrypted file. If the decrypted data is used before the authenticity verdict is given, then it is possible to exploit a vulnerability with a non-authentic document.

Therefore, it is essential that the decrypted data remains private until the authenticity verdict is received. When it is stored in RAM, this is easy, but when it must be stored on disk, in the case of large files, then it is necessary to exploit some defensive strategies, discussed later in this document.

Size reduction by fragmentation

One possible strategy for encrypting/decrypting a large file may be to break it up into chunks that will fit in RAM.

It might be tempting to look at encryption modes used for encrypting large amounts of data, like XTS. In a nutshell, XTS performs encryption/decryption using a single key, but it uses an encryption process that is “tweaked” to each logical unit. XTS is often used for disk encryption, and in this case the logical unit is the disk “sector”. Under Linux, it is possible to use XTS with dm-crypt and its LUKS overlay.

XTS presents a rather interesting advantage for disk encryption. Indeed, the tweak for each logical unit is an intrinsic information about the disk: the position of the sector on the disk. This subtlety means that it is not necessary to store additional data (the tweak) for each sector. So there is no storage expense for encryption related data!

Unfortunately, XTS offers limited integrity protection. Of course, if you take an encrypted sector and move it to another sector on the disk, the tweek of the algorithm will be incorrect. Indeed, if a sector is to be decrypted as sector X, and we try to decrypt it as sector Y, then the tweak will be incorrect, and the decryption will be meaningless. However, XTS does not protect against replacing sector X with an earlier version of sector X, for example.

Moreover, by splitting the disk into encrypted sectors, each with a different tweak, there is no protection against truncation. If one takes a disk of size X and copies it to a disk of size Y, with Y < X, the cryptographic algorithm will not detect that data is missing.

From the errors or constraints of disk encryption modes, several lessons can be learned. If the data to be encrypted/decrypted is too large to be stored in RAM, and the solution is to split it into chunks, then care must be taken to ensure that the integrity of the chunks is strong.

This integrity must ensure that :

  • each section cannot be modified individually, even by replacement with a section of another encrypted message of comparable size;
  • the order of the sections cannot be modified;
  • it is not possible to add or remove sections without the entire large file being considered invalid.

Problem number 1 can be easily solved by using a separate encryption/decryption key per encrypted file, in combination with an algorithm and encryption mode that results in an authenticated encryption, such as AES-GCM.

Problem 2 can be easily solved by adding a counter to each encrypted data block. This represents a storage overhead that can be paid for when we are talking about file encryption and not disk encryption.

It might be possible to create a tweakable encryption mode that is also authenticated, for example by combining XTS and an HMAC. Alas, the consequence would be that the cryptographic operations would be in two passes (XTS then HMAC), which is a potentially unnecessary computational overhead if a better solution is available (and it is; see below :)).

Furthermore, XTS + HMAC would not protect against issue 3). Indeed, to counter 3), one method is to add the expected amount of chunks in the metadata of the encrypted file. This amount should be protected in integrity. This method is not original; it is used in the Merkle-Damgård cryptographic construction, and is used in particular by the SHA hash algorithms.

All these additions are as many ways to make mistakes when performing the encryption and decryption steps. However, as stated in the chapter of this article, going off the beaten track is often synonymous with vulnerability.

Therefore, it would be better not to reinvent the wheel, and to use well-known cryptographic mechanisms and libraries to solve our large file problem.

Cryptographic libraries

In Go, there are various high-level cryptographic libraries that are frequently used. Here I will talk about OpenPGP, which is problematic, and NACL, which is to be preferred.

OpenPGP

OpenPGP is a fairly old encryption standard. Its main implementation is GnuPG, and it continues to be the hobby of some misguided technicians. Yes, I’m thinking in particular of you Linux distributions.

These harsh words against this format are however deserved. OpenPGP is a museum of horrors, full of antiquated mechanisms, and cryptographic constructs from the infancy of authenticated encryption. Also, and not least, its implementers seem to have a passion for bad API ideas. In fact, the author of this article discovered problems in most OpenPGP implementations in 2015, and some, in 2022, are still vulnerable to these findings… including GnuPG.

In Go, unsurprisingly, the OpenPGP implementation also contains some bad ideas. The package has even been frozen and deprecated, with the comment that it is not desirable for Go developers to use OpenPGP, as this format is “complex, fragile, and unsafe, and using it exposes applications to a dangerous ecosystem”. To make the point, we will study one of its problems.

While it is true that it is fairly universal for data sources to implement io.Reader, it is possible to question the relevance of this choice for an encrypted data source whose integrity can only be verified after a complete pass.

One might expect the OpenPGP container openpgp.MessageDetails to perform this check on its own when instantiated with openpgp.ReadMessage. This would be quite consistent with the encoding/gzip API whose NewReader function returns an error if there are no “magic” bytes at the beginning of the read. Alas, as said before, OpenPGP is a museum of horrors, and it is not possible to check the integrity of the encrypted document; it is necessary to decrypt the entire encrypted document first, to finally recover an integrity tag. Indeed, with the OpenPGP standard, the integrity tab (a simple SHA-1 of the cleartext) is part of the encrypted data, and is suffixed to the cleartext. This approach is called MAC-then-encrypt and is decried by the cryptographic community.

Although the io.Reader of openpgp.MessageDetails is stored in the aptly named UnverifiedBody field, it is extremely tempting for a developer to plug it into another io.Reader, like a series of decorators, and forget or discover too late that the message was not genuine!

NACL

NACL is an excellent cryptographic library, whose well-designed API allows only the most stubborn of idiots to make mistakes in its use. There are some command line tools to exploit it or its fork libsodium. One of them is the excellent minisign utility, by Frank Denis. The author of this article highly recommends minisign as a replacement for OpenPGP for signing documents!

There are implementations of minisign in Go, such as go-minisign, which unfortunately suffers from the same problem of handling large files that we are dealing with in this article. Fortunately it is possible to use go-minisign even for large files by using the tricks presented in this article, below.

Coming back to NACL, the box.Seal and box.Open functions have the particularity of not reading from an io.Reader and not writing to an io.Writer. So they do not fall into the crude trap at the bottom of which we find OpenPGP. These functions use byte slices. This could look like a blocking point. This article aims precisely at proposing a solution to circumvent this particularity, while offering a correct level of security.

System tips and tricks to the rescue

Control the release of data

As seen at the beginning of this article, it is important to control when the decrypted data is released; until the data is complete and verified, the working copy of the data must remain private. Since we are dealing with large files, which do not fit in RAM, it is necessary to store the working copy on the file system, while ensuring that no other process or task can access it. To do this, it is possible to use anonymous files.

Anonymous files are files that are stored on the file system without any links to them. By link, here, it is necessary to understand link in the sense “entry in a directory”: a hardlink. These files are created by specifying the O_TMPFILE option to syscall open(2). Any byte written to such a file is actually stored on the file system, via the file descriptor returned by open(2) and known only to the program that created it (and to the processes that will be poking around in /proc… but they are looking for trouble ;)). It is therefore a private copy of the decrypted file. When the file is complete and its content verified, it is then possible to publish it through different ways.

One way to publish the file in a not very elegant way is simply to create a new file, without the O_TMPFILE option, and then to copy the contents of the decrypted file into this new file which is accessible by the other processes. The file descriptor can then be closed, and the anonymous file will be automatically freed. This method is expensive and has the drawback of doubling the disk size needed to store the decrypted file, at least temporarily, until the file descriptor of the anonymous file is closed.

A more elegant way, which takes advantage of a feature that is not always available, is to use FICLONE of the syscall ioctl(2). FICLONE uses the copy-on-write (COW) functionality of some file systems, such as btrfs. With this syscall, it is possible to open a file with a hardlink and then request that the named file be a snapshot of the anonymous file. The two files will then share the same blocks of data on the file system, until one of them changes a block. But in this case, there will be no subsequent writing to the anonymous file after this call to ioctl(2). So this is simply a trick to link to the contents of the anonymous file, and thus publish it. The only drawback of this approach is that you have to use a file system that is compatible with FICLONE, and this is not the case with ext4, which is usually the default file system of Linux distrubutions.

Finally, there is a third method, also elegant, which does not take advantage of a particular feature of some file systems. Unfortunately, it requires certain system privileges to do so: CAP_DAC_READ_SEARCH. CAP_DAC_READ_SEARCH bypasses this file system protection, which is unfortunate, because it is also the privilege required to call the linkat(2) syscall, with the AT_EMPTY_PATH option. This syscall together with this option allows the creation of a link from a file descriptor. It allows giving a name to our anonymous file, once it is complete. It may be acceptable to give CAP_DAC_READ_SEARCH to our process, if it is running in a chroot in which this permission does not allow the program to gain or keep undue access to system resources. This solution is therefore probably acceptable under certain conditions, which must however be well controlled.

Decrypting a large file in virtual memory

Not everything that is in virtual memory is necessarily physical memory. Thus, it is possible to obtain a Go slice containing the contents of a file, without it being copied into RAM. In the same way, it is possible to write in a slice, which is not stored in RAM, thanks to a syscall: mmap(2). So we can call box.Seal and box.Open on such slices, and the result will have been computed without the content of the files being stored in RAM!

Unfortunately, things are never that “simple”. There are some additional subtleties required when performing this write operation to a slice pointing to a file placed in virtual memory with mmap(2). Firstly, it is necessary that the destination file is the right size before calling mmap(2). To do this, a sparse file can be created, using the fallocate(2) syscall. Then, once the writing to the slice is done, it is necessary to call the msync(2) syscall in order to force the transfer of data from the virtual memory to the file, before making the call to munmap(2), to free the slice created by mmap(2).

Similarly, mmap(2) should used for the encrypted file, but there are some subtleties here as well. In particular, it is better to work on a private copy of the file, rather than one that can be altered by an external source. Indeed, the behavior is not specified if an external program truncates the file after it has been passed to mmap(2).

Finally, when passing the slice receiving the decrypted data to box.Open, one must pass this slice with [:0], in order to keep the capacity of the slice equal to the size of the file, but to force its length to 0. By doing this, box.Open will not proceed to reallocations of the array underlying the slice. It is indeed very important to use this trick, in order to continue working in the virtual memory returned by mmap(2) and not to end up working accidentally in RAM.

Bringing it all together in a coherent program

To summarize everything that has been discussed in this article:

  • when dealing with a large file, it is better to use Linux efficiently than to risk creating vulnerabilities by trying to truncate the file;
  • it is important to always work with private copies of the data, both the encrypted and decrypted content;
  • it is necessary to have control over the publication of the decrypted content, in particular to ensure that the content has integrity and is complete before making it available to third-party applications.

To accomplish these goals, it is possible to use the Linux syscalls mmap(2), fallocate(2), msync(2), and ioctl(2) or linkat(2).

This git repository contains a library using all these elements to encrypt and decrypt a large file in a secure way.

Déchiffrement sécurisé de gros fichiers en Go

L’on dit souvent qu’il ne faut pas créer son propre algorithme cryptographique. Qu’en cryptographie, il ne faut pas faire preuve d’originalité ; que la sécurité est dans les sentiers battus, et en particulier ceux qui le sont par les cryptographes. Ce qu’on oublie souvent de dire, c’est qu’utiliser la cryptographie est aussi périlleux. Dans cet article, nous allons discuter du traitement des gros fichiers lors du déchiffrement, et de comment le faire de manière sécurisée sur un système Linux. Cet article sera illustré par du code Go.

Qu’est ce qu’un “gros fichier” et en quoi cela est-il différent des “petits fichiers” ?

Un petit fichier est tout ce qu’il est raisonnable de stocker en RAM, en intégralité. La RAM est un espace sanctuarisé où un programme peut stocker de l’information. Cette sanctuarisation offre avec une garantie relativement forte d’isolation contre les interférences provenant d’autres tâches/processus.

À l’inverse, un gros fichier est un fichier qui ne peut être stocké dans son intégralité dans la RAM du système qui va le déchiffrer. Dès lors des interférences peuvent survenir. Elles sont de plusieurs ordres.

Les modifications de la donnée chiffrée

Le premier problème est que la donnée chiffrée peut être modifiée par un programme externe. En effet, dès lors qu’un fichier est présent dans un répertoire, tout programme tournant avec les privilèges nécessaires peut ouvrir un descripteur de fichiers (file descriptor) sur ce fichier. Grâce à ce descripteur de fichiers, il est alors possible de modifier le fichier. Cela inclut les programmes tournant avec le même niveau de privilèges que le programme qui est en charge du déchiffrement.

Ce n’est pas forcément un problème. L’état de l’art en matière de cryptographie appliquée est d’utiliser du chiffrement authentifié. Ce type de chiffrement vérifie l’intégrité cryptographique du contenu chiffré, en même temps qu’il le déchiffre. Certains algorithmes et modes de chiffrement, comme AES-GCM ou AES-OCB, effectuent ces opérations en “une passe”. C’est-à-dire que la vérification de l’authenticité est effectuée au fur et à mesure du déchiffrement et un verdict d’authenticité est donné à la fin du déchiffrement. Hélas, tous les modes de chiffrement ne sont pas en une passe ; ainsi AES-CCM, par exemple, va d’abord effectuer une vérification en intégrité, puis effectuer le déchiffrement si la donnée a initialement été déterminée comme authentique. Hélas, un attaquant est alors en mesure d’altérer la donnée entre la première et la deuxième passe. Le contenu déchiffré est alors déclaré authentique, alors que ce dernier a été modifié.

En conséquence, lorsqu’un mode de déchiffrement en une passe n’est pas employé, il est nécessaire d’utiliser une copie privée de la donnée, que ce soit en RAM ou par d’autres astuces systèmes qui seront détaillées ultérieurement dans ce document.

Utilisation anticipée d’une donnée dont l’authenticité n’est pas encore assurée

Lorsque les données déchiffrées sont trop grandes pour être contenues en RAM, il est nécessaire de les écrire sur disque. Hélas, des problèmes de sécurité peuvent survenir lors de ce passage sur disque.

La liste de ces problèmes ne saurait être exhaustive, mais il est possible de penser notamment à certains logiciels qui utiliseraient la donnée de manière anticipée. En effet, certains logiciels surveillent le contenu d’un dossier avec inotify(7) ; c’est le cas de la plupart des explorateurs de fichiers.

Cette lecture anticipée peut résulter en des interprétations incorrectes du fichier. Ce n’est cependant pas la conséquence la plus funeste. Dans le cas d’un chiffrement en une passe, ou d’une vérification d’intégrité effectuée après le déchiffrement (c.f. la section de ce document relative à OpenPGP), il est possible que le fichier contenant le déchiffré intègre du code malveillant ajouté par un attaquant ayant corrompu le document chiffré. Il est, en effet, capital de comprendre que le chiffrement seul n’est pas suffisant pour garantir l’intégrité d’une donnée, et qu’il est nécessaire d’utiliser un motif d’intégrité, et de le vérifier, avant de faire usage du déchiffré. S’il est fait usage de la donnée déchiffrée avant que le verdict d’authenticité ne soit rendu, alors il est possible d’exploiter une vulnérabilité avec un document non-authentique.

En conséquence, il est indispensable que le déchiffré reste privé, jusqu’à réception du verdict d’authenticité. Lorsqu’il est stocké en RAM, c’est chose aisée, mais lorsqu’il doit être stocké sur le disque, dans le cas des gros fichiers, alors il est nécessaire d’exploiter quelques stratégies défensives, discutées plus bas dans ce document.

Réduction de la taille par fragmentation

Une stratégie possible pour chiffrer/déchiffrer un gros fichier peut être de le découper en tronçons qui tiendront en RAM.

Il pourrait être tentant de regarder du côté des modes de chiffrement utilisés pour le chiffrement de vastes quantité de données, comme XTS. En quelques mots, XTS effectue un chiffrement/déchiffrement à l’aide d’une unique clé, mais d’un procédé de chiffrement qui est ajusté (“tweaked”) à chaque unité logique. XTS est souvent employé pour le chiffrement de disques, et dans ce cas, l’unité logique est le “secteur” du disque. Sous Linux, il est possible de faire usage de XTS notamment avec dm-crypt et sa surcouche LUKS.

XTS présente un avantage assez intéressant pour le chiffrement de disques. En effet, l’élément qui permet de paramétrer l’ajustement (tweak) pour chaque unité logique est une donnée extérieure et intrinsèque du disque : la position du secteur sur le disque. Cette subtilité fait qu’il n’est pas nécessaire de stocker une donnée supplémentaire (les paramètres d’ajustement) pour chaque secteur. Il n’y a donc pas de dépense de stockage pour des données liées au chiffrement !

Hélas, XTS offre une protection en intégrité limitée. Certes, si l’on prend un secteur chiffré et qu’on le déplace dans un autre secteur du disque, les paramètres d’ajustement de l’algorithme vont être incorrects. En effet, si un secteur doit être déchiffré en tant que secteur X, et qu’on tente de le déchiffrer en tant que secteur Y, alors les paramètres de l’algorithme seront incorrects, et le déchiffré n’aura aucun sens. Cependant XTS ne protège pas contre le remplacement du secteur X par une version antérieure du secteur X, par exemple.

En outre, en découpant le disque en secteurs chiffrés, chacun avec un paramètre différent, il n’existe pas de protection contre la troncature. Si l’on prend un disque de taille X et qu’on le copie sur un disque de taille Y, avec Y < X, l’algorithme cryptographique ne détectera pas qu’il manque de la donnée.

Des erreurs ou contraintes des modes de chiffrement de disques, plusieurs leçons peuvent être tirées. Si l’on dispose d’une donnée à chiffrer/déchiffrer qui est trop grande pour être stockée en RAM, et que la solution envisagée est de découper cette donnée en tronçon, alors il faut veiller à mettre en place une intégrité forte des tronçons.

Cette intégrité doit assurer que :

  1. chaque tronçon ne peut être modifié individuellement, même par remplacement avec un tronçon d’un autre message chiffré de taille comparable ;
  2. l’ordre des tronçons ne peut être modifié ;
  3. il n’est pas possible d’ajouter ou de retirer des tronçons sans que l’ensemble du gros fichier ne soit considéré comme invalide.

La problématique numéro 1 peut être aisément résolue en utilisant une clé de chiffrement/déchiffrement distincte par fichier chiffré, en combinaison avec un algorithme et un mode de chiffrement qui permette d’obtenir un chiffré authentifié, comme AES-GCM.

La problématique 2 peut être aisément résolue en ajoutant un compteur à chaque bloc de données chiffré. Cela représente un surcoût de stockage qu’il est possible de payer quand on parle de chiffrement de fichiers et non de chiffrement de disques.

Il pourrait être envisageable de créer un mode de chiffrement ajustable qui soit également authentifié, par exemple en combinant XTS et un HMAC. Hélas, la conséquence serait que les opérations cryptographiques seraient en deux passes (XTS puis HMAC), ce qui représente un surcoût potentiellement inutile si une meilleure solution est disponible (et c’est le cas ; voir ci-dessous :)).

En outre, XTS + HMAC ne protègerait pas contre la problématique 3). En effet, pour protéger contre cette dernière, une méthode est d’ajouter la quantité de tronçons attendue en meta-données du fichier. Cette quantité devrait être protégée en intégrité. Cette méthode n’est pas originale ; elle est utilisée dans la construction cryptographique Merkle-Damgård, et est employée notamment par les algorithmes de hachage SHA.

Tous ces ajouts sont autant de manière de se tromper lorsqu’on réalise les étapes de chiffrement et de déchiffrement. Or, comme dit en chapô de cet article, sortir des sentiers battus est souvent synonyme de vulnérabilité.

Dès lors, il serait préférable de ne pas réinventer la roue, et d’employer des mécanismes et des bibliothèques cryptographiques bien connues pour résoudre notre problématique de gros fichiers.

Les bibliothèques cryptographiques

En Go, il existe diverses bibliothèques cryptographiques haut-niveau qui sont fréquemment employées. Je vais ici parler d’OpenPGP, qui est problématique, et de NACL, qu’il convient de préférer.

OpenPGP

OpenPGP est un standard de chiffrement assez ancien. Son implémentation principale est GnuPG, et il continue d’être la marotte de certains techniciens bien mal avisés. Oui, je pense notamment à vous, les distributions Linux.

Ces mots durs contre ce format sont cependant mérités. OpenPGP est un musée des horreurs, remplis de mécanismes vétustes, et de constructions cryptographiques datant des balbutiements du chiffrement authentifié. Également, et non des moindres, ses implémenteurs semblent avoir une passion pour les mauvaises idées en matière d’API. L’auteur de cet article a d’ailleurs découvert en 2015 des problèmes dans la plupart des implémentations d’OpenPGP, et certaines, en 2022, sont toujours vulnérables à ces découvertes… dont GnuPG.

En Go, sans surprise, l’implémentation d’OpenPGP contient elle aussi de mauvaises idées. Le package a même été gêlé et déprécié, avec le commentaire qu’il n’est pas souhaitable que les développeurs Go emploient OpenPGP, ce format étant “fragile, complexe, non sûr, et […] son usage expose les développeurs à un écosystème dangereux”. Pour enfoncer le clou, nous allons procéder à une étude de l’une de ses problématiques.

S’il est vrai qu’il est assez universel que des sources de données implémentent io.Reader, il est possible de s’interroger sur la pertinence de ce choix pour une source de données chiffrée dont l’intégrité ne peut être vérifiée qu’après une passe complète.

L’on pourrait alors s’attendre à ce que le containeur OpenPGP openpgp.MessageDetails effectue cette vérification de lui-même lors de son instanciation avec openpgp.ReadMessage. Cela serait assez cohérent avec l’API de encoding/gzip dont la fonction NewReader renvoie une erreur en l’absence d’octets “magiques” en début de lecture. Hélas, comme dit précemment, OpenPGP est un musée des horreurs, et il n’est pas possible de vérifier l’intégrité du chiffré ; il est nécessaire de déchiffrer l’intégralité du document chiffré, pour récupérer enfin un motif d’intégrité. En effet, avec le standard OpenPGP, le motif d’intégrité (un simple SHA-1 du clair) fait partie des données chiffrées, et est suffixé au clair. Cette approche est appelée MAC-then-encrypt et est décriée par la communauté cryptographique.

Bien que le io.Reader de openpgp.MessageDetails soit stocké dans le bien-nommé champ UnverifiedBody, il est extrêmement tentant pour un développeur de le “brancher” dans un autre io.Reader, à la manière d’une série de décorateurs, et d’oublier ou de découvrir trop tard que le message n’était pas intègre !

NACL

NACL est une excellente bibliothèque cryptographique, dont l’API, bien conçue, ne permet qu’aux plus entêtés des idiots de se tromper dans son emploi. Il existe quelques outils en ligne de commande pour l’exploiter, elle ou sa variante (fork) libsodium. On peut notamment citer l’excellent utilitaire minisign, par Frank Denis. L’auteur de cet article recommande vivement minisign comme remplacement à OpenPGP pour la signature de documents !

Il existe des implémentations de minisign en Go, dont go-minisign, qui hélas souffre du même problème de gestion des gros fichiers qui nous occupe dans cet article. Fort heureusement il est possible d’exploiter go-minisign y compris pour des gros fichiers en utilisant les astuces qui sont présentées dans le présent article, plus bas.

Pour en revenir à NACL, les fonctions box.Seal et box.Open ont pour particularité de ne pas recevoir d’io.Reader et d’écrire dans des io.Writer. Elles ne tombent donc pas dans le piège grossier au fond duquel on trouve OpenPGP. Ces fonctions utilisent des slices de bytes. Cela pourrait ressembler à un point bloquant. Cet article vise précisément à proposer une solution pour contourner cette particularité, tout en offrant un niveau de sécurité correct.

Astuces systèmes à la rescousse

Contrôler la mise à disposition des données

Comme vu en début d’article, il est important de maitriser le moment où les données déchiffrées sont publiées ; tant que ces dernières ne sont pas complètes ou pas vérifiées, la copie de travail de ces données doit rester privée. Vu que nous traitons des gros fichiers, qui ne tiennent pas en RAM, il est nécessaire de stocker la copie de travail sur le système de fichiers, tout en s’assurant que nul autre processus ou tâche ne puisse y avoir accès. Pour ce faire, il est possible d’utiliser les fichiers anonymes.

Les fichiers anonymes sont des fichiers qui sont stockés sur le système de fichiers, sans qu’aucun lien n’existe vers ces derniers. Par lien, ici, il faut comprendre lien dans le sens “entrée dans un répertoire” (hardlink). Ces fichiers sont créés en spécifiant l’option O_TMPFILE au syscall open(2). Tout octet écrit dans un tel fichier est effectivement stocké sur le système de fichiers, par l’intermédiaire du descripteur de fichier renvoyé par open(2) et connu uniquement du programme qui l’a créé (et des processus qui vont farfouiller dans /proc… mais ils cherchent les problèmes ;)). Il s’agit donc d’une copie privée du déchiffré. Lorsque le fichier est complet et son contenu vérifié, il est alors possible de le publier de différentes manières.

Une manière de publier le fichier d’une manière peu élégante est simplement de créer un nouveau fichier, sans l’option O_TMPFILE, puis de recopier le contenu du fichier déchiffré dans ce nouveau fichier qui est accessible par les autres processus. Le descripteur de fichiers peut alors être fermé, et le fichier anonyme sera automatiquement libéré. Cette méthode est couteuse et présente le défaut de doubler la taille disque nécessaire pour stocker le déchiffré, au moins temporairement, jusqu’à la fermeture du descripteur de fichier du fichier anonyme.

Une manière plus élégante, qui tire cependant partie d’une fonctionnalité qui n’est pas toujours disponible, est d’utiliser FICLONE du syscall ioctl(2). FICLONE utilise la fonctionnalité de copy-on-write (COW) de certains systèmes de fichiers, comme btrfs. Avec ce syscall, il est possible d’ouvrir un fichier ayant un lien (hardlink) puis de demander que le fichier nommé soit une copie instantanée du fichier anonyme. Les deux fichiers partageront alors les mêmes blocs de données sur le système de fichiers, jusqu’à ce que l’un d’entre eux modifie un bloc. Mais en l’espèce, il n’y aura pas d’écriture ultérieure dans le fichier anonyme, après cet appel à ioctl(2). Il s’agit donc simplement d’une astuce qui permet de créer un lien vers le contenu du fichier anonyme, et donc de le publier. Le seul défaut de cette approche est qu’il faut utiliser un système de fichier compatible avec FICLONE, et ce n’est notamment pas le cas de ext4, qui est généralement le système de fichiers par défaut des distrubutions Linux.

Finalement, il existe une troisième méthode, elle aussi élégante, qui ne tire par partie d’une fonctionnalité particulière de certains systèmes de fichiers. Hélas, il est nécessaire de disposer de certains privilèges système pour ce faire : CAP_DAC_READ_SEARCH. CAP_DAC_READ_SEARCH permet de contourner cette protection du système de fichiers, ce qui est regrettable, car il s’agit aussi du privilège requis pour appeler le syscall linkat(2), avec l’option AT_EMPTY_PATH. Ce syscall cumulé à cette option permet de créer un lien à partir d’un descripteur de fichiers. Il permet donc de donner un nom à notre fichier anonyme, une fois celui-ci complet. Il peut être acceptable de donner CAP_DAC_READ_SEARCH à notre processus, si ce dernier est exécuté dans un chroot dans lequel cette permission ne permet pas au programme de gagner ou conserver des accès indûs à des ressources du système. Cette solution est donc probablement acceptable dans certaines conditions, qui doivent cependant être bien maitrisées.

Travailler sur une copie inaltérable du chiffré

Dans le cas d’un procédé de déchiffrement en deux passes, une pour la vérification d’intégrité, et une pour le déchiffrement en lui-même, il est possible d’utiliser les mêmes mécanismes que détaillés dans la section précédente de cet article pour obtenir une copie privée : créer un fichier anonyme dont le contenu sera alimenté par recopie (avec un surcoût en espace disque) ou le syscall ioctl(2) avec FICLONE pour obtenir une copie instantanée et qui ne pourra être modifiée par des processus ou taches tiers.

Déchiffrer un gros fichier en mémoire virtuelle

Tout ce qui est dans la mémoire virtuelle n’est pas nécessairement de la mémoire physique. Ainsi, il est possible d’obtenir une slice Go comportant le contenu d’un fichier, sans que celui-ci ne soit lu et recopié en RAM. De même, il est possible d’écrire dans une slice, qui n’est pas stocké en RAM, grâce à un syscall : mmap(2). Il nous est donc possible d’appeler box.Seal et box.Open sur de telles slices, et le résultat aura été calculé sans que le contenu des fichiers ne soit stocké en intégralité en RAM !

Hélas, les choses ne sont jamais aussi “simples”. Il a quelques petites subtilités supplémentaires, requises lorsqu’on effectue cette opération d’écriture dans une slice pointant vers un fichier placé en mémoire virtuelle avec mmap(2). D’une part, il est nécessaire que le fichier de destination soit de la bonne taille avant l’appel à mmap(2). Pour cela, on peut créer un fichier creux (sparse file), à l’aide de l’appel système fallocate(2). Ensuite, une fois l’écriture dans la slice accomplie, il est nécessaire d’appeler le syscall msync(2) afin de forcer le transfert de données depuis la mémoire virtuelle vers le fichier, avant d’effectuer l’appel à munmap(2), pour “défaire” la slice créée par mmap(2).

De même, mmap(2) est utilisé pour le fichier chiffré, mais il y a là aussi quelques subtilités. Il est notamment préférable de travailler sur une copie privée du fichier, plutôt qu’un fichier altérable par une source externe. En effet, le comportement n’est pas spécifié si un programme externe tronquait le fichier après que ce dernier ait été passé à mmap(2).

Enfin, lorsqu’on passe la slice recevant les données déchiffrées à box.Open, il convient de passer cette slice découpée avec [:0], afin de garder la capacité de la slice égale à la taille du fichier, mais de forcer sa longueur à 0. Ce faisant, box.Open ne procèdera pas à des réallocations du tableau qui sous-tend la slice. Il est, en effet, capital d’user de cette astuce, afin de continuer de travailler dans la mémoire virtuelle retournée par mmap(2) et ne pas se retrouver à travailler accidentellement en RAM.

Rassembler tout en un programme cohérent

Pour résumer tout ce qui a été abordé dans cet article :

  • pour traiter un gros fichier, il est préférable d’utiliser efficacement Linux que de risquer de créer des vulnérabilités en essayant de tronçonner le fichier ;
  • il est important de toujours travailler avec des copies privées des données, que ce soit le contenu chiffré et le contenu déchiffré ;
  • il est nécessaire d’avoir un contrôle sur la publication du déchiffré, notamment pour s’assurer que le contenu est intègre, et complet avant de le rendre accessible à des applications tierces.

Pour accomplir ces objectifs, il est possible d’exploiter les syscall Linux mmap(2), fallocate(2), msync(2), et ioctl(2) ou linkat(2).

Ce dépôt git contient un exemple d’algorithme mettant tous ces éléments en oeuvre, pour déchiffrer un gros fichier, de manière sécurisée.