Ce blog n'a d'autre prétention que de me permettre de mettre à la disposition de tous des petits textes que j'écris. On y parle surtout d'informatique mais d'autres sujets apparaissent parfois.
Date de publication du RFC : Août 2022
Auteur(s) du RFC : P. Faltstrom (Netnod), F. Ljunggren (Kirei), D. van Gulik (Webweaving)
Pour information
Première rédaction de cet article le 10 août 2022
Ce RFC décrit un encodage en texte de données binaires, l'encodage Base45. Proche conceptuellement d'encodages comme Base64, il est, par exemple, beaucoup utilisé pour le code QR.
Ces codes QR ne permettent pas de stocker directement des données binaires quelconques car le contenu sera toujours interprété comme du texte. Il est donc nécessaire d'encoder et c'est le rôle de Base45. Il existe bien sûr d'innombrables autres mécanismes d'encodage en texte, comme Base16, Base32 et Base64, spécifiés dans le RFC 4648 mais Base45 est plus efficace en terme de place occupée.
Base45 utilise un sous-ensemble d'ASCII, de 45 caractères (d'où son nom). Deux octets du contenu binaire sont encodés en trois caractères de ce sous-ensemble. Par exemple, deux octets nuls donneront la chaine de caractères "000". Regardons tout de suite avec le module Python base45 :
% pip3 install base45 % python3 >>> import base45 >>> base45.b45encode(b'Bonjour, tout le monde') b'.H86/D34ENJES4434ESUETVDL44-3E6VC' >>>
Et décodons avec le module Elixir base45 :
% iex -S mix iex(1)> Base45.decode(".H86/D34ENJES4434ESUETVDL44-3E6VC") "Bonjour, tout le monde"
C'est parfait, tout le monde est d'accord. Ici, évidemment, la chaine originale n'avait pas vraiment besoin d'être encodée donc on va essayer avec du binaire, les trois octets 42, 1 et 6, encodage en Elixir, décodage en Python :
iex(1)> Base45.encode(<<42, 1, 6>>) "/D560" >>> base45.b45decode("/D560") b'*\x01\x06'
Encore une fois, tout va bien, Elixir a encodé les trois octets du binaire en quatre caractères, que Python a su décoder (l'astérisque est affiché car son code ASCII est 42).
Je vous laisse découvrir l'algorithme complet (il est assez simple) dans la section 3 du RFC. Si vous le programmez vous-même (ce qui n'est sans doute pas une bonne idée, il existe déjà de nombreuses mises en œuvre), attention aux cas limites comme un binaire d'un seul octet, ou comme une chaine à décoder qui compte des caractères invalides. Ici, un essai avec un caractère invalide, le signe égal :
% python3 >>> import base45 >>> base45.b45decode("AAAA=") Traceback (most recent call last): File "/home/stephane/.local/lib/python3.8/site-packages/base45/__init__.py", line 30, in b45decode buf = [BASE45_DICT[c] for c in s.rstrip("\n")] File "/home/stephane/.local/lib/python3.8/site-packages/base45/__init__.py", line 30, in <listcomp> buf = [BASE45_DICT[c] for c in s.rstrip("\n")] KeyError: '=' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/home/stephane/.local/lib/python3.8/site-packages/base45/__init__.py", line 54, in b45decode raise ValueError("Invalid base45 string") ValueError: Invalid base45 string
Pensez à tester votre code avec de nombreux cas variés. Le RFC cite
l'exemple des chaines "FGW" (qui se décode en une paire d'octets
valant chacun 255) et "GGW" qui, quoique proche de la précédente et
ne comportant que des caractères de l'encodage Base45 est néanmoins
invalide (testez-la avec votre décodeur). Le code de test dans le module Elixir
donne des idées de tests utiles (certains viennent de bogues
détectées pendant le développement). Regardez
test/base45_test.exs
.
Et, comme toujours, lorsque vous recevez des données venues de l'extérieur, soyez paranoïaques dans le décodage ! Un code QR peut être malveillant et chercher à activer une bogue dans le décodeur (section 6 du RFC).
Enfin, le RFC rappelle qu'une chaine encodée n'est pas forcément directement utilisable comme URL, elle peut comporter des caractères qui ne sont pas sûrs, il faut donc encore une étape d'encodage si on veut en faire un URL.
Compte tenu de l'importance des codes QR, on trouve des mises en œuvre de Base45 un peu partout. J'ai par exemple cité celle en Python. Si vous programmez en Elixir, il y a une bibliothèque sur Hex (écrite en Erlang) mais aussi ma bibliothèque (aux performances très basses).
Sinon, si vous cherchez un bon article d'explication sur Base45, je recommande celui-ci.
Auteur(s) du livre : Florian Besson, Pauline
Ducret, Guillaume Lancereau, Mathilde
Larrère
Éditeur : Les Arènes
979-10-375-0590-3
Publié en 2022
Première rédaction de cet article le 5 août 2022
Le parc d'attractions du Puy du Fou est un grand succès commercial, qui attire de nombreux visiteurs. Bon, d'accord, mais le Parc Astérix aussi, et on n'écrit pas des livres à son sujet, pourtant. Mais le Puy du Fou a une autre caractéristique : ses promoteurs prétendent qu'il est historiquement fondé, et qu'on peut non seulement s'y amuser mais en plus apprendre l'histoire. Quatre historiens se sont donc associés pour aller au parc d'attraction et vérifier.
Il y a plusieurs écueils si on veut avoir un regard critique sur l'histoire tendance Puy du Fou. Il y a le risque d'une critique élitiste, faisant fi du succès populaire du parc, méprisant la vulgarisation, et estimant que l'histoire ne doit se traiter que dans de gros livres ennuyeux pour universitaires. Il y a aussi le risque du pinaillage, pointant des erreurs de détails, par exemple sur les dates, et oubliant que le Puy du Fou a une cohérence dans sa vision de l'histoire, et que c'est elle qu'il faut étudier.
C'est ce qu'ont fait les quatre auteur·es, dont au moins deux sont activement engagés dans la vulgarisation de l'histoire, notamment sur les réseaux sociaux (les deux autres aussi, peut-être, mais je ne les connais pas). Ils sont allés au Puy du Fou, ils ont apprécié les spectacles, ils ont pris des notes et ils analysent la représentation historique. S'il y a en effet d'innombrables erreurs factuelles dans les spectacles du Puy du Fou, le plus gros problème est la vision peu historique du parc : une France éternelle, gardant son identité inchangée à travers les siècles, mélange de l'idéologie de l'Ancien Régime et du roman national du XIXe siècle. Par exemple, le spectacle se déroulant en Gaule romaine présente les seuls Gaulois comme ancêtres des Français, comme si la France d'aujourd'hui n'était pas tout autant héritière des Romains. (En prime, le spectacle montre les Gaulois chrétiens et les Romains païens, ce qui ne correspond pas à la réalité de l'époque.) Les auteur·es analysent de nombreux spectacles et la plupart (je vous laisse découvrir les exceptions) présentent la même vision d'une France mythique, qui a peu de rapport avec la réalité et avec les vrais Français des différentes époques.
Critiquer, c'est facile, mais proposer est plus difficile, pourront dire les défenseurs du parc. Mais un des chapitres les plus intéressants du livre est justement celui où les auteur·es se font scénaristes et imaginent quatre spectacles qui gardent ce qu'il y a de bien au Puy du Fou (des animations de qualité et spectaculaires, avec pyrotechnie obligatoire) tout en étant plus rigoureux historiquement. Ainsi celui de l'Antiquité montre les débats et divergences entre premiers chrétiens, alors que le Puy du Fou montre le christianisme, comme il le fait de la France, comme une entité éternelle et figée.
Faut-il aller au Puy du Fou ? Là n'est pas la question. Personnellement, je n'y suis jamais allé mais le livre peut donner envie, les spectacles semblant passionnants et bien faits. Le tout est d'y aller comme à Disneyland, pour la distraction, et pas en prétendant qu'on suit un cours d'histoire.
Auteur(s) du livre : Mark Corcoral
Éditeur : L'Harmattan
978-2-343-22452-7
Publié en 2021
Première rédaction de cet article le 4 août 2022
On le sait, l'attribution d'une cyber-attaque (« c'est les Chinois ! ») est un exercice difficile. Il faut analyser l'attaque, parvenir à une certitude et, ensuite, assumer de révéler publiquement l'origine. Comme le répète souvent Guillaume Poupard « L'attribution, c'est politique ». Le livre de Mark Corcoral étudie cette question de l'attribution notamment sous l'angle des accusations par les États-Unis : comment se fait l'attribution publique et suivant quels méandres politiques ?
La phrase de Guillaume Poupard, citée plus haut, a un amusant double sens. Elle dit que ce n'est pas à une agence technique comme l'ANSSI d'attribuer une attaque ; cette attribution publique peut avoir des conséquences et c'est donc au pouvoir politique de prendre ses responsabilités. Mais la phrase dit aussi qu'accuser publiquement tel ou tel pays est aussi un choix politique. Plus cyniquement, on pourrait reprendre la phrase d'un collègue de Topaze : « les coupables, il vaut mieux les choisir que les chercher ». Eh oui, quand un État en accuse un autre, l'accusation n'est pas forcément sincère…
Déjà, se faire une opinion est difficile : contrairement à une attaque physique qui va forcément laisser pas mal d'indices, une cyber-attaque ne laisse pas beaucoup de traces, et celles-ci peuvent facilement être imitées (une chaine de caractères en hébreu dans un logiciel malveillant ne signifie pas forcément que c'est un coup du Mossad, il est trivial d'en copier/coller une). Et l'interprétation de ces IOC peut être délicate. J'ai entendu des analystes expliquer que, comme les opérations de la cyber-attaque étudiée prenaient place entre 9 h et 17 h, heure de Beijing, cela menait à soupçonner les Chinois, comme si l'APL était tenue aux horaires de bureau. Et même une fois qu'on a acquis une conviction, il n'est pas facile d'en convaincre des tiers puisque, presque toujours, ceux qui font l'attribution d'une attaque ne donnent aucune information concrète sur les preuves récoltées (pour ne pas donner d'informations aux ennemis, mais aussi parfois pour cacher la faiblesse des preuves).
Et une fois qu'on a son intime conviction, l'attribution publique n'est pas automatique, elle relève de choix politiques, qui peuvent évoluer. C'est ainsi qu'avant, en gros, 2014, les États-Unis ne se livraient pas à des attributions publiques, avant de changer de politique et de multiplier les accusations. (La Russie et la Chine continuent à ne pas faire d'attribution publique pour des attaques précises, même si ces deux pays dénoncent de façon très générale les cyber-attaques dont ils sont victimes. Ils insistent beaucoup sur la difficulté à produire des preuves convaincantes.) Et à partir de 2017, les attributions faites par les États-Unis ne sont plus unilatérales mais impliquent parfois des pays alliés. Le choix dépend de la culture politique de chaque pays, et de sa conviction que l'attribution publique servira à quelque chose, par exemple en terme de propagande, ou bien pour faire avancer des négociations internationales sur une certaine régulation des attaques ou encore pour intimider un adversaire (« je t'ai vu ! »). Le choix est complexe et l'auteur explique très bien les innombrables questions géopolitiques qui sont liées à l'utilisation (ou pas) de l'attribution publique. Cette analyse des raisons qui poussent à attribuer publiquement est le gros du livre.
Un ouvrage que je recommande beaucoup, pour comprendre la complexité des questions de cyber-attaques et la difficulté des décisions à prendre.
(En plus léger, l'excellente BD « Cyberfatale » tourne essentiellement autour d'une question d'attribution, comment savoir qui a fait le coup et, une fois qu'on le sait, que faire de cette information.)
Auteur(s) du livre : Frédéric Amiel
Éditeur : Les éditions de l'Atelier
978-2-7082-5375-9
Publié en 2021
Première rédaction de cet article le 3 août 2022
Si, comme moi, vous abusez du chocolat, ce livre va peut-être vous être désagréable : il explique l'évolution de la mondialisation pour le cas particulier du chocolat. Ce n'est pas toujours très rose.
On commence évidemment au Mexique, où les conquérants européens découvrent le chocolat (que les Aztèques consommaient sans sucre mais avec du piment) et le rapportent en Europe. La substance suit le parcours de beaucoup d'autres produits rapportés d'Amérique : méfiance (médicale, mais aussi religieuse, quelque chose d'aussi bon doit être une création du diable), engouement, snobisme (rare et cher, le chocolat pouvait être un marqueur de distinction sociale). Puis la faible production mexicaine ne suffit plus et les Européens vont commencer à cultiver le chocolat de manière plus massive. D'abord aux Antilles puis en Amérique du Sud. Mais si on veut produire beaucoup de chocolat, il ne faut pas seulement avoir beaucoup de cacaoyers, il faut aussi traiter les fèves et, à la main, c'est long et difficile. Le parcours du chocolat croise donc celui de James Watt : ses machines à vapeur permettent de commencer à mécaniser le traitement. On n'est plus à la production artisanale, le chocolat est devenu une industrie. On croise dans le livre des entrepreneurs variés, comme Menier, un des promoteurs du paternalisme.
Puis la production de chocolat passe en Afrique. Le livre suit le développement d'une production de plus en plus importante, notamment sur l'ile de São Tomé, puis sur le continent. Un nouveau problème se pose alors : la production, grâce à la mondialisation de la production et à l'industrialisation, est devenue abondante et bon marché. Il faut donc convaincre de nouveaux consommateurs, pour faire du chocolat un produit de masse. Au 19e siècle, l'industrie chocolatière est donc une des plus friandes de publicité. L'exemple le plus connu de publicité pour le chocolat est, en France, le tirailleur sénégalais de Banania.
Et dans les pays producteurs ? Les cours du cacao sur les marchés mondiaux ne sont pas stables et ces pays connaissent des alternances de prospérité et de vaches maigres. Bien des gouvernements, après l'indépendance des colonies, ont cherché à stabiliser la situation, avec des succès variables. Car, entre temps, le cacao était passé des marchés de produits physiques aux marchés financiers. Désormais, au lieu de vendre et d'acheter du cacao, on achète et on vend des titres ayant un rapport plus ou moins lointain avec le cacao. (Note au passage : quand des gens, en 2022, reprochent aux cryptomonnaies d'être « virtuelles » et « déconnectées de l'économie réelle », ils ont quelques dizaines d'années de retard. Cela fait bien longtemps que les échanges sur les marchés sont déconnectés des produits et services.)
L'auteur se penche ensuite sur les alternatives : est-ce que du chocolat bio ou équitable (ou, soyons fous, les deux à la fois) permettrait au consommateur d'avoir de meilleurs produits et aux producteurs de vivre mieux ? La question des labels est compliquée ; certains n'ont guère de valeur car ils sont spécifiques à une entreprise, qui décide des critères et fait leur « vérification ». Difficile pour le consommateur de s'y retrouver !
Bref, un livre très lisible et très pédagogique, expliquant en détail tous les aspects de la production d'une marchandise, dans un monde où c'est devenu très compliqué.
Auteur(s) du livre : Nicolas Arpagian
Éditeur : L'observatoire
979-10-329-2134-0
Publié en 2022
Première rédaction de cet article le 3 août 2022
Vous trouvez que la situation géopolitique sur l'Internet est compliquée ? Au moins, le livre d'Arpagian vous permettra d'avoir un tour d'horizon complet d'un certain nombre d'enjeux importants.
Ne lisez pas le quatrième de couverture, qui abuse de sensationnalisme (« les geeks-soldats de la cyber guérilla précèdent désormais les chars d'assaut »), voire d'erreurs (« la Russie construit son propre Internet »). Le livre est plus sérieux. Centré sur le rôle des frontières (pas seulement les frontières entre États, mais aussi celles qu'on s'était imaginées entre des activités que le numérique redéfinit), il a l'avantage de contenir de nombreux faits et données. Il passe en revue beaucoup de questions (sans doute trop, vu sa taille, et sans que ces questions aient de relation claire) : la privatisation, la censure, le pouvoir des GAFA, les mensonges, la cybersécurité, le succès de Léna Situations… C'est sérieux, l'auteur connait son sujet, et il ne prend pas pour agent comptant les clichés, par exemple sur le soi-disant anonymat de l'Internet.
Un livre qui peut être utile pour celle ou celui qui débute son exploration de la politique sur Internet. Vous pouvez aussi vous en faire une idée avec cet interview vidéo de l'auteur.
PS : le titre est un nom de domaine, réservé mais qui ne semble pas utilisé.
Première rédaction de cet article le 2 août 2022
Sur le fédivers, jonny a attiré l'attention sur une discussion un peu oubliée, qualifiée avec une certaine exagération de « une des premières polémiques sur Internet ». Il s'agissait d'un débat en 1979 sur un changement technique fait dans des serveurs finger pour protéger un peu la vie privée, changement qui n'avait pas fait consensus. Un sujet toujours d'actualité, même dans l'Internet très différent que nous avons aujourd'hui.
Pour comprendre le débat, il faut passer un paragraphe à expliquer finger. Normalisé (mais bien après les faits) dans le RFC 1288, finger était un service d'information très populaire, qui permettait de trouver des informations sur un utilisateur d'une machine distante, son numéro de téléphone, son adresse postale, s'il était connecté en ce moment, etc. Avec les yeux d'aujourd'hui, on voit bien les questions de vie privée que cela peut poser. Le changement qui avait été fait dans un serveur finger (à l'université Carnegie-Mellon) était de ne pas diffuser l'information la plus sensible (dernière connexion, et dernière lecture du courrier électronique) par défaut (mais l'utilisateur avait une option pour activer cette diffusion).
Et c'est là que la polémique, en 1979, a commencé. Vous pouvez lire l'archive de la liste de diffusion msggroup de l'époque et voir la discussion de nos jours suite au pouète de jonny. Quel était le problème avec le changement fait à Carnegie Mellon ? C'était que ne pas diffuser l'information semblait à beaucoup anti-social. Le débat semble assez surréaliste aujourd'hui, avec une forte pression sociale pour tout diffuser par défaut. Mais, comme le notent Arne Babenhauserheide et Antoine-Frédéric dans la discussion, les mécanismes permettant d'indiquer qu'un message a été lu (qui sont déployés par exemple par WhatsApp) posent presque les mêmes problèmes (l'information n'est pas publique toutefois).
La loi Informatique & Libertés existait déjà, pourtant, mais on peut penser qu'elle n'était pas connue du petit groupe qui faisait alors fonctionner l'Internet, et au sein duquel tout le monde était à la fois utilisateur et acteur.
Il y a plein de choses à remarquer dans cette discussion. D'abord, que des choix apparemment très techniques (deux bits dans la configuration pour indiquer si on diffuse ces deux informations) sont en fait politiques. La passion déclenchée par ce simple changement (qui concernait un seul serveur finger : ceux qui n'étaient pas d'accord n'étaient pas obligés de modifier le leur) montre bien que tout le monde était conscient de l'importance de la question. Notons toutefois que des opposants au changement fait par l'université affirmaient que c'était un changement « politique », manifestement en considérant que ce terme était une insulte.
Ensuite, on peut noter bien sûr que les mentalités ont changé : on n'imagine plus aujourd'hui affirmer que garder ces informations privées est anti-social. Mais les changements ne sont pas si importants que cela ; encore aujourd'hui, lorsqu'on dénonce des atteintes à la vie privée, il y a toujours quelqu'un pour dire « si vous ne faites rien de mal, vous n'avez pas de raison d'avoir une vie privée ».
À propos de mentalité, il y a un cliché courant aujourd'hui qui est de dire que l'Internet n'avait pas été conçu pour la sécurité, c'est parce que, à l'époque, ses utilisateurs/acteurs étaient tous des hippies barbus et universitaires qui vivaient dans un monde de bisounours et se faisaient confiance. Il est vrai que ce monde était assez restreint, avec une forte homogénéité de classe sociale, de pays (uniquement des étatsuniens), de profession et de mentalité. Et l'espoir était en effet qu'une mentalité d'ouverture, de franche discussion et de confiance allait se généraliser. Ce n'était bien sûr pas réaliste, un tel climat pouvait se maintenir dans un petit groupe cohérent, mais avait peu de chance de passer à l'échelle, dans un Internet aux dimensions du monde. (Ceci dit, je préfère l'optimisme, même erroné, au cynisme « de toute façon, c'est la nature humaine, la loi de la jungle, la société sera toujours violente, et il faut mettre des barbelés partout ».)
Mais si les acteurs de l'Internet de l'époque sous-estimaient les questions de sécurité, ils ne les ignoraient pas. Sur bien des points où on reproche aux pionniers de l'Internet de n'avoir pas pris en compte la sécurité dès le début, il faut remarquer que, d'abord, il y a des bonnes raisons pour ne pas encombrer un nouveau système avec de la sécurité, si on veut qu'il soit un succès (RFC 5218). Ensuite, même aujourd'hui, avec l'expérience et le recul, on serait bien en peine de déployer un système sécurisé à l'échelle mondiale, avec le cahier des charges de l'Internet, qui est notamment de permettre à toute Alice de parler à tout Bob. Bref, les questions de 1979 n'appartiennent pas à un passé définitivement révolu, elles se posent toujours aujourd'hui.
Et un autre point à noter : l'importance de la valeur par défaut. Le changement fait à Carnegie Mellon impactait tous les utilisateurs (je n'utilise pas l'écriture inclusive ici car je soupçonne qu'il y avait peu d'utilisatrices), à part la minorité qui change les réglages. Ce problème est toujours d'actualité, par exemple lorsque Facebook se défend des reproches sur son manque de protection de la vie privée en disant que les utilisateurices peuvent toujours changer les réglages (tout en sachant que très peu le feront).
Auteur(s) du livre : Kate Brown
Éditeur : Penguin Books
978-0-141-98854-2
Publié en 2019
Première rédaction de cet article le 23 juillet 2022
Ce livre est une analyse des conséquences à long terme de la catastrophe nucléaire de Tchernobyl. Que se passe-t-il dans une région profondément irradiée ? L'auteure explore en détail le traitement de la catastrophe dans les années qui ont suivi l'explosion du réacteur, en ne se fiant pas uniquement aux déclarations officielles. Un ouvrage évidemment plutôt douloureux à lire, mais qui ne laisse aucun détail de côté.
Tout le monde connait la catastrophe qui a vu l'explosion d'un réacteur nucléaire à Tchernobyl le 26 avril 1986. On ne mentionne souvent que ses conséquences à court terme (la mort des « liquidateurs », qui sert à des affirmations trompeuses comme « la catastrophe n'a fait que 31 [le chiffre varie] morts ») mais le livre de Brown se concentre sur des conséquences à plus long terme, moins spectaculaires. Et il n'est pas facile de mesurer ces conséquences à long terme. Prenons l'exemple du cancer de la thyroïde. On en observe chez des enfants dans la région. Mais il y a aussi des cancers de ce type en l'absence de contamination radioactive. Sont-ils plus fréquents après l'accident de la centrale ? La statistique est une science difficile. (Comme on l'a vu récemment avec la pandémie de Covid-19, qui a vu le retour de déclarations anti-statistiques du genre « mon beau-frère n'est pas vacciné, et bien il n'est pas tombé malade ».)
Et il est d'autant plus difficile de répondre à la question que l'accident s'est produit dans un pays où le système de santé était loin d'être parfait, avec une collecte peu fiable des données (et le cancer de la thyroïde est apparemment délicat à diagnostiquer). En outre, il y avait sur place une radioactivité artificielle qui n'était pas due à cet accident, mais qui provenait des essais nucléaires atmosphériques et d'autres accidents (l'URSS traitait la sécurité nucléaire avec beaucoup de légèreté). Bref, le traitement de ces données n'est pas évident. Toutefois, il est clair que la radioactivité sur place est plus importante que ce qui avait parfois été raconté par les autorités, et que les maladies sont bien en augmentation.
Une autre difficulté statistique que décrit bien l'auteure est que la radioactivité est très variable d'un endroit à l'autre, et ne s'en tient pas aux traits tracés sur la carte (« ici, évacuation nécessaire, ici, ça peut aller »). Un jardin portager peut être contaminé et celui d'à côté rester peu touché. Un exemple particulièrement frappant est donné par l'auteure où des cueilleuses vont récolter des myrtilles dans une forêt et où chaque panier est mesuré pour déterminer sa contamination radioactive. Bien que récoltés dans la même forêt, ils sont très différents. Et, pour rester en deçà des normes de radioactivité acceptable, l'organisateur décide de mélanger les myrtilles des paniers relativement sains avec celle des paniers contaminés, diluant ainsi la radioactivité jusqu'à ce qu'elle reste en dessous du seuil…
Le livre décrit également longuement les réactions des autorités dans les années ayant suivi la catastrophe. Tchernobyl est en Ukraine mais proche de la Biélorussie. Dès l'accident, avant même la fin de l'URSS, les autorités ukrainiennes et biélorusses avaient traité le problème très différemment, la Biélorussie choisissant de largement nier le problème, y compris en confisquant les compteurs Geiger (pas de mesures, pas de problème…). La controverse n'est évidemment pas purement scientifique et Brown décrit longuement toutes les manœuvres des autorités pour cacher, ou relativiser, les faits gênants. Et Moscou s'en mêlait également, mais pas de façon uniforme : dans ces années de perestroïka, l'État soviétique partait déjà à vau-l'eau et tout le monde n'avait pas, et de loin, les mêmes idées sur comment faire face au problème. (À un moment, ce sont des médecins du KGB qui mettent en évidence les mensonges rassurants des autorités : vu les privilèges du KGB, ils avaient une meilleure clinique et du meilleur matériel, et savaient donc bien ce qu'il en était.)
Ah, et en parlant de géopolitique, l'auteure met aussi en évidence une curieuse complicité entre l'URSS et les USA. Malgré le fait que la guerre froide n'était pas finie, pas mal d'intervenants étatsuniens sur place soutenaient le discours officiel soviétique comme quoi les conséquences de l'accident n'étaient pas si graves que ça. Ce n'était pas forcément par confiance aveugle dans leurs collègues soviétiques, mais aussi peut-être par crainte que l'image de marque des filières nucléaires (civiles et militaires) ne souffrent trop de l'accident.
Bref, un livre à lire, pour celui ou celle qui veut approfondir les controverses politico-scientifiques, et apprécier la complexité des faits.
Première rédaction de cet article le 22 juillet 2022
La lecture d'une excellente étude faite à l'université de Twente m'a motivé pour faire un court article sur une question qu'on se pose trop rarement : sur un réseau comme l'Internet, les paquets IP passent-il vraiment là où le protocole de routage, typiquement BGP, leur a dit de passer ?
Je vous divulgâche tout de suite la fin : non. Comme répètent souvent les administrateurs réseau anglophones, « The data plane does not always follow the control plane ». Qu'est-ce que cela veut dire ?
Expliquons un peu le routage, dans un
réseau comme l'Internet : les routeurs se parlent entre eux et
échangent des informations (du genre « Tu sais quoi ? Je sais
comment joindre 2001:db8:fa9:aa3::/64
. ») et
ces informations échangées leur servent à construire leur
table de routage, une structure de données qui
associe aux préfixes IP (comme ce
2001:db8:fa9:aa3::/64
), l'adresse du routeur
suivant, celui qui rapprochera le paquet du
but. Ensuite, lorsqu'un routeur reçoit un paquet, il consulte sa
table de routage et envoie le paquet au routeur suivant, via la
bonne interface. Deux choses sont à noter :
Sur l'Internet, le protocole de routage standard est BGP (RFC 4271). Les annonces BGP ont la forme « je sais joindre
2001:db8:fa9::/48
et je le sais parce que
l'AS 64500 me l'a
dit et il le tenait de l'AS 65539 ». Mais, du fait du routage par
étapes indépendantes, rien ne dit qu'un paquet envoyé au routeur qui
a fait cette annonce ira bien à l'AS 64500 puis à l'AS 65539. Chaque
AS (en pratique, un
AS = un opérateur) est indépendant et a sa propre politique de
routage. Par exemple, l'administratrice du routeur suivant a
parfaitement pu configurer des routes statiques prioritaires, qui
seront utilisées avant celles apprises via BGP. C'est le sens de la
phrase « The data plane does not always follow the control
plane ». « The data plane », c'est IP,
c'est la transmission. « The control plane »,
c'est le protocole de routage, par exemple BGP.
L'étude
de Koen van Hove citée plus haut est une illustration
pratique de ce point (je vous en recommande vraiment la
lecture). L'auteur travaille sur la validation de l'origine des
routes (ROV, pour Route Origin Validation), via
la RPKI
(Resource Public Key Infrastructure). Les
titulaires de préfixes IP publient (et
signent) des ROA (Route Origin
Authorization) qui indiquent quel AS peut être à l'origine
d'une route (dans l'exemple plus haut, 65539 était l'origine). Les
routeurs connectés à l'Internet peuvent utiliser ces ROA pour
valider les annonces. Si on trouve un ROA correctement signé qui dit
« 2001:db8:fa9::/48
doit avoir comme AS
d'origine 65539 » et qu'une annonce
« 2001:db8:fa9::/48
passe par moi et ça vient
de l'AS 65500, qui en est l'origine », une telle annonce, invalide,
peut être rejetée.
Est-ce que cela suffit à garantir la sécurité du routage ? Non,
car, montre l'étude, même si on rejette cette annonce invalide, les
paquets qu'on émet sont transmis à des AS différents et ceux-ci
peuvent avoir d'autres règles et, par exemple, ne pas valider. Donc,
le malheureux paquet destiné à
2001:db8:fa9:b3:551::12:af8
sera peut-être
finalement transmis au méchant AS 65500. C'est ennuyeux, mais c'est
logique, chaque AS étant maitre de sa politique de routage.
Mais est-ce qu'on ne pourrait pas changer cela et « forcer » les AS à respecter des décisions prises par l'émetteur du paquet ? (Ce qu'on nomme le routage par la source.) Non, on ne peut pas, et le problème n'est pas technique (il existe plusieurs mécanismes pour représenter de telles décisions dans les paquets), mais d'ordre commercial et politique. En termes simples, les autres opérateurs font ce qu'ils veulent, et ils n'ont pas de raison de vous obéir. (Il faut se rappeler que l'Internet est une fédération de réseaux, pas un réseau unique.) Ils ont d'autant moins de raisons d'obéir à d'éventuelles consignes de l'émetteur que celles-ci permettraient des attaques contre la sécurité, par exemple en forçant le passage par un chemin qu'un attaquant sait moins contrôlé.
Première rédaction de cet article le 6 juillet 2022
Ce mardi 5 juillet 2022, l'organisme de normalisation étatsunien NIST a annoncé qu'il avait choisi les algorithmes de cryptographie post-quantiques qu'il allait maintenant normaliser. Ce sont Kyber pour l'échange de clés et Dilithium pour les signatures.
L'annonce était prévue plus tôt mais a été retardée, selon certains par des problèmes liés aux nombreux brevets qui grèvent ces algorithmes (l'annonce du NIST prévoit des négociations), selon d'autres par la difficulté qu'avait la NSA à attaquer les algorithmes proposés . Il est difficile de le savoir puisque le NIST, même s'il avait fait un effort d'ouverture à cette occasion, reste assez opaque. En tout cas, désormais, c'est fait.
Pour comprendre l'importance de cette annonce, il faut revenir sur celle de la cryptographie. On sait que cet ensemble de techniques est absolument indispensable à la sécurité de l'utilisateur sur l'Internet. Ne croyez pas les politiciens ou les éditorialistes qui vont vous expliquer que si vous ne faites rien de mal, vous n'avez rien à cacher et pas besoin de cryptographie. Cet argument a été réfuté d'innombrables fois depuis des années mais continue à être parfois utilisé pour pousser les citoyen·nes à communiquer en public. Ignorez-le, et chiffrez l'intégralité de vos communications, c'est indispensable dans le monde d'aujourd'hui.
Mais aucune technique de sécurité n'est parfaite, que cela soit la cryptographie ou une autre. Il y a des questions non-techniques mais aussi des limites techniques de la cryptographie. Notamment, dans l'ordre d'importance décroissante :
Ce dernier risque est le plus spectaculaire et celui que les geeks préfèrent mentionner. Mais, en pratique, il est sans doute le moins sérieux. Vous avez bien plus de chances de voir vos secrets percés suite à une imprudence ou une maladresse de votre correspondant·e ou de vous-même que suite à une découverte mathématique fondamentale résolvant le problème du logarithme discret et cassant ainsi ECDSA. Certes, un problème mathématique comme la décomposition en facteurs premiers, qui est à la base de RSA, reste un problème ouvert et on n'a jamais démontré qu'il était insoluble. Néanmoins, vu le nombre de gens qui l'ont attaqué depuis des dizaines d'années, on peut être raisonnablement confiant : le problème est manifestement très difficile, et RSA reste donc sûr. La seule méthode connue pour attaquer des algorithmes comme ECDSA ou RSA reste donc la force brute, l'examen systématique de toutes les possibilités, ce qui prend quelques milliards d'années avec les machines existantes. (Je simplifie : on dispose en fait d'algorithmes meilleurs que la pure force brute mais ils ne font pas de miracles non plus.) En outre, la difficulté augmente exponentiellement avec la taille de la clé, et un progrès dans les processeurs ou bien certaines optimisations des programmes de cryptanalyse peuvent facilement être annulés en agrandissant la clé.
Mais les calculateurs quantiques pourraient changer les choses. Je ne vais pas détailler ici le fonctionnement de ces machines. Je dirais juste que, reposant directement sur la quantique, ils permettent de faire tourner des algorithmes radicalement différents (et pas évidents du tout à programmer !) de ceux qui sont utilisés actuellement, sur les ordinateurs classiques. Ainsi, l'algorithme de Shor permet de décomposer un nombre en ses facteurs premiers, en un temps linéaire, et donc de trouver une clé privée RSA à partir de la clé publique, tâche qui était impossible aux ordinateurs classiques. L'algorithme de Shor ne tournant que sur des calculateurs quantiques, si on dispose d'un tel calculateur, on a cassé RSA (et, avec un algorithme proche, ECDSA et les autres algorithmes à courbes elliptiques). Ce serait une catastrophe pour la sécurité de l'Internet, puisque les communications confidentielles pourraient toutes être décryptées, et les signatures toutes imitées. (Notez que cela serait pareil, même sans calculateur quantique, si un·e mathématicien·ne génial·e découvrait demain un moyen simple de décomposer un nombre en facteurs premiers. Comme indiqué plus haut, c'est peu probable mais pas impossible.)
Mais attention, le « si on dispose d'un calculateur quantique » est un gros « si ». De même qu'Euclide avait développé des algorithmes et qu'Ada Lovelace avait écrit des programmes longtemps avant qu'un ordinateur ne soit disponible pour les exécuter, Shor a développé son algorithme sans avoir de calculateur quantique. On a progressé depuis et des calculateurs quantiques existent, et on peut faire tourner l'algorithme de Shor dessus. Le problème est qu'ils sont très expérimentaux, très peu existent dans le monde et leurs plus grands exploits sont très loin de ce qui serait nécessaire pour casser les clés d'aujourd'hui. Les optimistes parient que ce n'est qu'une question de temps et que, dans quelques années, les calculateurs quantiques (dont les enthousiastes prédisent depuis des années qu'ils sont presque au point) s'attaqueront facilement à nos algorithmes cryptographiques. Les pessimistes font remarquer que les difficultés pratiques qui se présentent face aux calculateurs quantiques sont colossales et que les résoudre n'est pas un simple travail d'ingéniérie prévisible mais est souvent plus proche de la recherche fondamentale et de ses incertitudes. Bref, il n'y a pas actuellement de consensus sur le temps dont nous disposons encore face à la « menace quantique ».
Comme il faut s'y prendre à l'avance pour développer, tester et déployer de nouveaux algorithmes dits post-quantiques, les cryptographes n'ont pas attendu de voir les calculateurs quantiques en vente chez Darty pour réagir. Le travail sur des algorithmes post-quantiques a commencé il y a longtemps. Ces nouveaux algorithmes doivent résister aux calculateurs quantiques. Plus exactement, il faut qu'on ne connaisse pas d'algorithme quantique pour les attaquer. Mais il faut aussi qu'ils résistent à la cryptanalyse classique (voir par exemple cette attaque contre Rainbow, un des finalistes du concours). Et il faut aussi qu'ils soient réalistes en performance, en taille de clés, etc. Plusieurs candidats prometteurs ont été développés, et les cryptanalystes se sont déjà fait les dents à essayer de les casser.
Sur l'Internet, mais aussi en général dans le monde numérique, la normalisation est essentielle. Avoir des protocoles et des algorithmes, notamment de cryptographie, communs, permet l'interopérabilité, qui garantit liberté de choix, et bon fonctionnement du réseau. Pour qu'Alice puisse envoyer un message à Bob, il faut bien qu'Alice chiffre avec un algorithme que Bob puisse déchiffrer. Le rôle des organisations de normalisation est donc crucial. C'est pour cela que le NIST étatsunien a lancé en 2016 un grand concours pour choisi le meilleur algorithme à normaliser. Plutôt que de faire travailler ensemble des cryptologues à développer en commun ce meilleur algorithme, ce qui risquait d'amener à un compromis qui ne satisferait personne, le NIST a choisi le concours : de nombreuses équipes proposent leur algorithme, chacun essaie de casser celui des concurrents et que le meilleur gagne. C'est ce concours qui vient de franchir une étape décisive, avec l'annonce du choix de :
Maintenant que ces algorithmes ont été choisis, verra-t-on dès demain TLS, SSH, DNSSEC et les autres utiliser des algorithmes post-quantiques ? Non, car il reste plusieurs étapes à franchir. D'abord, le NIST doit finir le travail de normalisation : il faut spécifier rigoureusement l'algorithme et publier la spécification officielle. Cela implique, comme le note l'annonce officielle, une négociation réussie sur les brevets qui plombent le champ de la cryptographie post-quantique (cf. par exemple le problème avec le CNRS et l'accord ultérieur). Ensuite, la plupart des utilisations de la cryptographie se fait au sein d'un protocole de cryptographie comme TLS. Les protocoles sérieux disposent tous de la propriété d'agilité (RFC 7696), ce qui veut dire qu'ils ne sont pas lié à un algorithme de cryptographie particulier. Mais il faudra quand même spécifier l'utilisation du nouvel algorithme pour ce protocole (par exemple, pour DNSSEC, il faudra l'ajouter au registre IANA, ce qui nécessitera la publication d'un RFC). (Cet article donne une première idée du travail à faire.) Ensuite, les programmeur·ses devront programmer ces algorithmes post-quantiques (une tâche à effectuer soigneusement ; on a vu que les bogues sont à l'origine de bien des problèmes de cryptographie). Même s'il existe déjà plusieurs mises en œuvre de ces algorithmes, des programmes dignes d'être utilisés en production sont une autre affaire. Et il faudra ensuite déployer ces programmes dans le monde réel.
Pour la partie de normalisation dans les protocoles TCP/IP, ce qui relève de l'IETF, on peut noter que quelques RFC parlent déjà de post-quantique. C'est le cas par exemple des RFC 8784, RFC 8696, RFC 8773 et RFC 8784, mais c'est exagéré, ils décrivent simplement des clés de cryptographie symétrique pré-partagées. Plus sérieusement, le RFC 8032 discute les risques que les calculateurs quantiques posent à l'algorithme EdDSA, et le RFC 8391 le fait pour XMSS, tandis que le RFC 9180 (section 9.1.3) a une vision plus générale. Même discussion pour les RFC 8240 et RFC 8576, dans le contexte de l'Internet des objets. Le RFC 9021 doit être un des rares qui repose déjà sur un algorithme post-quantique mais on voit que la prise de conscience était ancienne.
Il faudra donc compter de nombreuses années avant d'avoir les algorithmes post-quantiques massivement déployés. C'est d'ailleurs pour cela qu'il faut commencer tout de suite le processus, alors même que la menace des calculateurs quantiques est lointaine. (On peut aussi se rappeler que la cryptographie sert parfois à protéger des secrets de longue durée. Si les fichiers que vous chiffrez actuellement seront toujours sensibles dans trente ans, dites-vous bien que les calculateurs quantiques capables de casser les clés utilisées seront peut-être une réalité dans trente ans.)
Le mot « quantique » est parfois mis à toutes les sauces. Il faut notamment signaler que les questions discutées ici n'ont rien à voir avec ce qu'on nomme parfois la « cryptographie quantique » (et qui serait mieux appelée « distribution quantique de clés »), une technique dont l'intérêt n'est pas du tout évident (voir aussi le démontage de cette idée par Dan Bernstein et l'avis de l'ANSSI, en anglais seulement). De même, la question de la cryptographie post-quantique n'a pas de lien direct avec les projets d'« Internet quantique » (où les routeurs sont quantiques), sur lesquels travaille entre autres l'IRTF dans son groupe QIRG.
Sinon, question mises en œuvre, Kyber et Dilithium sont dans la bibliothèque de Cloudflare (écrite en Go). Elle n'a apparemment aucune documentation et son utilisation semble difficile. (La bibliothèque de Microsoft avait fait le choix d'autres algorithmes.)
Quelques bonnes lectures sur cette question des algorithmes de cryptographie post-quantiques :
Et merci à Manuel Pégourié-Gonnard et Damien Stehlé pour leur relecture attentive. Évidemment, les erreurs restantes sont de moi.
Auteur(s) du livre : Tania Louis
Éditeur : humenSciences
978-2-3793-1194-9
Publié en 2020
Première rédaction de cet article le 26 juin 2022
Je recommande ce passionnant livre de Tania Louis qui fait le tour de la question des virus, un sujet d'actualité (mais le livre a été largement écrit avant la pandémie de Covid-19, dont il ne parle pas).
Première chose que j'ai apprise dans ce livre : la virologie, c'est compliqué, d'autant plus que, régulièrement, des découvertes remettent en cause ce qu'on croyait avoir bien établi. Ainsi, la règle que les virus soient plus petits que les bactéries ne tient plus depuis la découverte des mimivirus.
Deuxième chose, la question de savoir si les virus sont vivants ou pas. L'auteure estime que, oui, plutôt, on peut dire qu'ils sont vivants, mais cela dépend évidemment de la définition exacte qu'on donne de la vie, et les virus, comme vu plus haut, ont tendance à défier les classifications trop rigides (j'ai d'ailleurs été surpris par la première partie de ce livre, qui ne parle pas avant longtemps de biologie, mais qui explique cette question de la classification). Au moins, les virus obligent à se poser des questions sur ce que l'on croit savoir de la vie. L'auteure note, par exemple, qu'on se focalise peut-être trop sur la particule virale, alors que le « vrai » virus est plutôt ce qui est actif dans la cellule.
Troisième découverte (pour moi), les virus ne sont pas forcément néfastes. On a bien sûr surtout étudié ce qui causaient du mal aux humains ou à l'agriculture. Mais il existe de nombreux virus, qui ne sont pas forcément dangereux.
Mais cela ne veut pas dire qu'ils sont inactifs. Les virus sont bien équipés pour faire passer des gènes d'un organisme à un autre et c'est un puissant coup de main à l'évolution. Un organe comme le placenta, si utile à nous autres mammifères placentaires, semble bien devoir son existence à des virus.
Les virus peuvent aussi aider les humains par l'action qu'ils ont contre certains de nos ennemis. Des virus infectent et tuent des bactéries dangereuses, par exemple. Ces bactériophages ont été au début du vingtième siècle considérés comme un moyen prometteur de lutter contre les infections bactériennes (travaux de Félix d'Hérelle, personnage passionnant). La mise au point des antibiotiques a fait un peu oublier les virus bactériophages, sauf dans le bloc de l'Est que la guerre froide tenait un peu à l'écart des exportations étatsunienne, comme le Coca-Cola ou les antibiotiques. L'apparition des résistances aux antibiotiques redonne leur chance aux bactériophages qui seront peut-être d'utiles alliés.
Le livre détaille plusieurs aventures où la science est faite par des êtres humains, avec leurs qualités, mais aussi leurs défauts. À propos de Rosalind Franklin, par exemple, l'auteure explique bien le processus compliqué de la recherche scientifique et pourquoi il est faux de présenter Franklin comme ayant fait tout le travail sur l'ADN seule (légende courante aujourd'hui), tout comme il était erroné de l'avoir complètement oubliée pendant de nombreuses années.
Comme le note l'auteure, la virologie évolue sans cesse, dépêchez-vous donc de lire ce livre avant qu'il ne soit plus d'actualité.
Date de publication du RFC : Juin 2022
Auteur(s) du RFC : R. Bush (Arrcus & IIJ Research), R. Housley (Vigil Security)
Chemin des normes
Réalisé dans le cadre du groupe de travail IETF sidrops
Première rédaction de cet article le 15 juin 2022
Un très court RFC pour un simple rappel, qui ne devrait même pas être nécessaire : les identités utilisées dans la RPKI, la base des identités qui sert aux techniques de sécurisation de BGP, n'ont pas de lien avec les identités attribuées par l'État et ne doivent pas être utilisées pour, par exemple, signer des documents « officiels » ou des contrats commerciaux.
Le RFC 6480 décrit cette RPKI : il s'agit d'un ensemble de certificats et de signatures qui servent de base pour des techniques de sécurité du routage comme les ROA du RFC 6482 ou comme le BGPsec du RFC 8205. Les objets de la RPKI servent à établir l'autorité sur des ressources Internet (INR, Internet Number Resources, comme les adresses IP et les numéros d'AS). Le RFC 6480, section 2.1, dit clairement que cela ne va pas au-delà et que la RPKI ne prétend pas être la source d'identité de l'Internet, ni même une source « officielle ».
Prenons un exemple concret, un certificat
choisi au hasard dans les données du RIPE-NCC (qu'on peut récupérer avec
rsync en
rsync://rpki.ripe.net/repository
). Il est au
format DER donc ouvrons-le :
% openssl x509 -inform DER -text -in ./DEFAULT/ztLYANxsM7afpHKR4vFbM16jYA8.cer Certificate: Data: ... Serial Number: 724816122963 (0xa8c2685453) Validity Not Before: Jan 1 14:04:04 2022 GMT Not After : Jul 1 00:00:00 2023 GMT Subject: CN = ced2d800dc6c33b69fa47291e2f15b335ea3600f ... X509v3 extensions: ... sbgp-ipAddrBlock: critical IPv4: 51.33.0.0/16 ... sbgp-autonomousSysNum: critical Autonomous System Numbers: 206918 ...
Je n'ai pas tout montré, mais seulement les choses importantes :
51.33.0.0/16
, utilisant les extensions du
RFC 3779.Ce certificat dit simplement que l'entité qui a la clé privée (RFC 5280) correspondante (une administration britannique, dans ce cas) a autorité sur des ressources comme l'AS 206918. Rien d'autre.
Mais, apparemment, certaines personnes n'avaient pas bien lu le RFC 6480 et croyaient que cet attirail PKIesque leur permettait également de signer des documents sans lien avec les buts de la RPKI, voire n'ayant rien à voir avec le routage. Et d'autant plus que des gens croient que le I dans RPKI veut dire Identity (il veut en fait dire Infrastructure). Il a ainsi été suggéré que les clés de la RPKI pouvaient être utilisées pour signer des LOA demandant l'installation d'un serveur dans une baie.
Pourquoi est-ce que cela serait une mauvaise idée ? Il y a
plusieurs raisons mais la principale est qu'utiliser la RPKI pour
des usages en dehors du monde restreint du routage Internet est que
cela exposerait les AC de cette RPKI à des
risques juridiques qu'elles n'ont aucune envie d'assumer. Et cela
compliquerait les choses, obligeant sans doute ces AC à déployer des
processus bureaucratiques bien plus rigides. Si on veut connaitre
l'identité officielle (que le RFC nomme à tort « identité dans le
monde réel ») d'un titulaire de ressources Internet, on a les bases
des RIR,
qu'on interroge via RDAP ou autres
protocoles. (C'est ainsi que j'ai trouvé le titulaire de
51.33.0.0/16
.) Bien sûr, il y a les
enregistrements Ghostbusters du RFC 6493 mais ils sont uniquement là pour aider à trouver un
contact opérationnel, pas pour indiquer l'identité étatique du
titulaire. Même les numéros d'AS ne sont pas l'« identité » d'un acteur de
l'Internet (certains en ont plusieurs).
Notons qu'il y a d'autres problèmes avec l'idée de se servir de la RPKI pour signer des documents à valeur légale. Par exemple, dans beaucoup de grandes organisations, ce ne sont pas les mêmes personnes qui gèrent le routage et qui gèrent les commandes aux fournisseurs. Et, au RIPE-NCC, les clés privées sont souvent hébergées par le RIPE-NCC, pas par les titulaires, et le RIPE-NCC n'a évidemment pas le droit de s'en servir pour autre chose que le routage.
Bref, n'utilisez pas la RPKI pour autre chose que ce pour quoi elle a été conçue.
Date de publication du RFC : Juin 2022
Auteur(s) du RFC : M. Nottingham (Fastly), P. Sikora (Google)
Chemin des normes
Réalisé dans le cadre du groupe de travail IETF httpbis
Première rédaction de cet article le 14 juin 2022
Le protocole HTTP, qui est à la base du Web, n'est pas forcément de bout
en bout, entre client et serveur. Il y a souvent passage par un
relais et ce relais a
parfois des choses à signaler au client HTTP, notamment en cas
d'erreur. Ce RFC
spécifie le champ d'en-tête Proxy-Status
pour
cela.
La norme HTTP, le RFC 9110 décrit ces relais, ces intermédiaires, dans sa section 3.7. On en trouve fréquemment sur le Web. Il y a depuis longtemps des codes d'erreur pour eux, comme 502 si le serveur d'origine répond mal et 504 pour les cas où il ne répond pas du tout. Mais ce n'est pas forcément assez précis, d'où le nouveau champ dans l'en-tête (ou dans le pied). Il utilise la syntaxe des champs structurés du RFC 8941. Voici un exemple :
Proxy-Status: proxy.example.net; error="http_protocol_error"; details="Malformed response header: space before colon", "Example CDN"
Cet exemple se lit ainsi : le premier (en partant du serveur
d'origine) s'identifie comme proxy.example.net
et il signale que le serveur d'origine n'avait pas bien lu le RFC 9112. Puis la réponse est passée par un autre
intermédiaire, qui s'identifie comme "Example
CDN"
(l'identificateur n'est pas forcément un
nom de domaine), et n'a rien de particulier à
raconter. Le champ Proxy-Status
est désormais
dans
le registres des champs d'en-tête (ou de pied).
Vous avez vu dans l'exemple ci-dessus le paramètre
error
. Il peut s'utiliser, par exemple, avec le
code de retour 504 :
HTTP/1.1 504 Gateway Timeout Proxy-Status: foobar.example.net; error=connection_timeout
Ici, le relais foobar.example.net
n'a eu aucune
réponse du serveur d'origine (ou, plus rigoureusement, du serveur
qu'il essayait de contacter, qui peut être un autre
intermédiaire).
Mais il existe d'autres paramètres possibles, comme :
next-hop
: nom ou adresse du serveur
contacté, par exemple Proxy-Status: cdn.example.org;
next-hop=backend.example.org:8001
.next-protocol
: protocole
(ALPN,
RFC 7301) utilisé avec
le serveur, par exemple Proxy-Status:
"proxy.example.org"; next-protocol=h2
pour du HTTP/2
(RFC 9113).received-status
: le code de retour du
serveur, comme dans Proxy-Status: ExampleCDN;
received-status=200
, pour un cas où tout s'est bien
passé.Et on peut définir de nouveaux paramètres par la procédure d'examen par un expert (RFC 8126).
Le paramètre error
prend comme valeur un
type d'erreur. Il en existe
plusieurs, chacun avec un code de retour recommandé dont (je
ne les indique pas tous, ils sont très nombreux !) :
dns_timeout
(pour le code de retour
504) et dns_error
(code 502) : c'est la faute du
DNS. Le second type permet en outre
d'indiquer le paramètre rcode
(code de retour
DNS, comme NXDOMAIN
) et le paramètre
info-code
, le code étendu du RFC 8914.connection_timeout
ou connection_refused
.destination_ip_prohibited
: le
pare-feu ne veut pas.tls_protocol_error
: là, c'est
TLS qui ne veut pas.tls_certificate_error
:
certificat du serveur problématique, par
exemple expiré.http_protocol_error
: la réponse du
serveur n'était pas du bon HTTP.proxy_internal_error
: le relais a un
problème interne.Là aussi, on peut enregistrer de nouveaux types avec la procédure d'examen par un expert du RFC 8126.
La section 4 du RFC détaille les questions de sécurité. Comme
toute information, Proxy-Status
peut aider un
attaquant, par exemple en lui donnant des idées sur comment joindre
directement un intermédiaire. Pour cette raison, les logiciels
doivent fournir un moyen de contrôler l'ajout (ou pas) de
Proxy-Status
, qu'on peut aussi n'inclure que
dans certains cas. Notez aussi qu'un intermédiaire peut mentir (ou
se tromper) et que le Proxy-Status
ne vaut donc
que ce que vaut l'intermédiaire.
Je n'ai pas de liste des logiciels qui mettent en œuvre ce champ Proxy-Status
.
Date de publication du RFC : Juin 2022
Auteur(s) du RFC : M. Nottingham
Réalisé dans le cadre du groupe de travail IETF httpbis
Première rédaction de cet article le 10 juin 2022
Aujourd'hui, la grande majorité des API accessibles via le réseau fonctionnent au-dessus de HTTP. Ce nouveau RFC, qui remplace le RFC 3205, décrit les bonnes pratiques pour la conception de telles API, notamment pour les protocoles IETF bâtis sur HTTP, comme DoH ou RDAP.
Il y a plein d'applications qui fonctionnent au-dessus de HTTP. Ce nouveau RFC se concentre sur celles qui sont d'usage général et qui ont plusieurs mises en œuvre et déploiements. (Si vous faites un service centralisé qui n'a qu'un seul déploiement de son API spécifique, ce RFC ne va pas forcément être pertinent pour vous.) Si vous avez déjà lu le RFC 3205, il faudra tout recommencer, les changements sont nombreux. Ces applications utilisant HTTP sont parfois qualifiées de REST mais, en toute rigueur, toutes ne suivent pas rigoureusement les principes de REST. Notez aussi un sous-ensemble, CRUD, pour les applications dont l'essentiel du travail est de créer/supprimer/gérer des objets distants.
Normalement, HTTP avait été conçu pour le Web et ses usages. Mais on voit aujourd'hui de très nombreuses API réseau être fondées sur HTTP pour diverses raisons :
Mais tout n'est pas forcément rose et HTTP peut ne pas être bien adapté à ce qu'envisage le développeur de l'API. Et cette développeuse peut faire des erreurs lors de la conception de l'API, erreurs que ce RFC vise à éviter.
En effet, quand on développe une API sur HTTP, il y a plusieurs décisions à prendre :
La section 2 précise l'applicabilité de ce RFC. Il concerne les
protocoles qui utilisent HTTP (ports 80 ou
443, plans d'URI http:
ou
https:
). Ceux qui utiliseraient une version
modifiée de HTTP ne comptent pas, et cette pratique est d'ailleurs
déconseillée, puisque ces versions modifiées feraient probablement
perdre les avantages d'utiliser HTTP, notamment la réutilisation des
logiciels et infrastructures existants.
Section 3, maintenant. Quelles sont les caractéristiques
importantes de HTTP, qui gouvernent ce que peuvent faire les
applications qui l'utilisent ? D'abord, sa sémantique très
générale : on peut tout faire avec HTTP. Notammment, HTTP est
indépendant du type de ressources sur lesquelles il agit. Ainsi, des
composants HTTP génériques (bibliothèques, serveurs, relais) peuvent
être développés et déployés pour des applications très différentes,
même des applcations qui n'existent pas encore. (Voilà d'alleurs
pourquoi la section précédente insistait sur le fait q'il ne faut
pas modifier HTTP.) Plus subtile serait l'erreur qui consisterait à
spécifier un certain profil de HTTP, en restreignant ce que HTTP
peut faire ou pas (« la réponse à un POST
doit
être 201 »). Une telle restriction, là encore, empêcherait
d'utiliser cerrtains composants génériques, en faisant perdre à HTTP
de sa généralité.
Une autre erreur courante est de s'attribuer tout ou partie de
l'espace de nommage fourni par les liens
hypertextes. C'est par exemple le cas lorsqu'une
application estime qu'elle peut contrôler tout le chemin dans l'URI
et décider que /truc/machin
est forcément à
elle (RFC 8820). Cela complique le
déploiement, par exemple si on veut installer cette application sous
/chose
et excusivement sous ce chemin
(cf. section 4.4). L'application devrait au contraire permettre de
la souplesse et utiliser les possibilités qu'offre le système de
liens (RFC 8288).
Enfin, HTTP dispose de nombreuses possibilités comme le multiplexage que permettent HTTP/2 (RFC 9113) et HTTP/3 (RFC 9114), l'intégration avec TLS, la possibilité de relayage, la négociation de contenu, la disponibilité de nombreux clients, et l'application qui utilise HTTP doit donc veiller à ne pas casser cet écosystème, et en tout cas à ne pas réinventer la roue, alors que HTTP offre déjà de nombreuses solutions éprouvées.
Bref, compte-tenu de tout cela, comment faire pour bien utiliser HTTP dans sa nouvelle application ? La section 4 est là pour répondre à cette question.
D'abord, bien définir la dépendance de l'application à HTTP, en donnant comme référence RFC 9110 (et surtout pas une version spécifique de HTTP, toujours afin de profiter au maximum de l'écosystème existant). On notera quand même que DoH (RFC 8484) impose (section 5.2 de son RFC) au moins HTTP/2, pour être sûr d'avoir du multiplexage. Notre RFC permet explicitement ce genre d'exceptions.
Le RFC recommande également, quand on montre un dialogue HTTP
titre d'exemple, d'utiliser plutôt les conventions de HTTP/1 (RFC 9112), plus lisibles. Donc, par exemple,
GET /truc HTTP/1.1
plutôt que le
:method = GET :path = /resource
de
HTTP/2. C'est ce que fait curl :
% curl -v http://www.example > GET / HTTP/2 > Host: www.example > user-Agent: curl/7.68.0 > accept: */* > < HTTP/2 200 < content-Type: text/plain < content-Length: 13
On l'a dit, il ne faut pas modifier le comportement de base de HTTP. Ce qu'on peut faire, par contre :
Et le client ? L'application qui utilise HTTP ne devrait pas exiger de comportement trop spécifique du client ; idéalement, un navigateur Web normal devrait pouvoir interagir avec l'application. On peut par exemple s'appuyer sur les principes FETCH. Il est également préférable que l'application qui va utiliser HTTP soit claire sur le traitement attendu pour les redirections HTTP, ou pour les cookies (RFC 6265), et rappeler que la vérification des certificats doit se faire selon les principes de la section 4.3.4 du RFC 9110.
Le client doit, idéalement, pouvoir se configurer avec uniquement
un URL. (Par exemple, un serveur DoH est annoncé ainsi, comme
https://doh.bortzmeyer.fr/
, la seule
information dont vous avez besoin pour l'utiliser.) Et si on ne
connait qu'un nom de domaine ? La solution du chemin d'URL fixe
qu'on s'alloue
(« obligatoirement /app
») étant interdite (RFC 8820), il y a le choix :
Et le plan d'URI (le premier composant de l'URI) ?
http:
et surtout https:
sont évidemment recommandés mais on peut aussi choisir un plan
spécifique. Cela va évidemment rendre l'application inutilisable
par un navigateur Web ordinaire. Certains navigateurs permettent
d'enregistrer un mécanisme de gestion de ces plans non standards
(comme le registerProtocolHandler()
du WHATWG) mais cela ne
marche pas partout. Et on aura le même problème avec tout
l'écosystème logiciel de HTTP. Bref, utiliser un plan autre que
http:
ou https:
fera
perdre une bonne partie des avantages qu'il y avait à utiliser le
protocole HTTP. D'autres problèmes se poseront comme l'impossibilité
d'utiliser le concept d'origine (RFC 6454) par
exemple dans la Same Origin
Policy, ou comme d'autres fonctions utiles
de HTTP (cookies, authentification, mémorisation
- RFC 9111, HSTS - RFC 6797, etc). Si vous tenez encore, après tout ça, à créer
un plan à vous, consultez le RFC 7595.
Et les ports ? HTTP utilise par défaut les
ports 80 pour le trafic en clair et 443 pour le traffic
chiffré. Utiliser un autre port est possible
(https://machin.example:666
) mais rend le
trafic de l'application distinguable, ce qui peut être gênant pour
la vie privée. (C'est un des choix de conception de DoH que d'utiliser HTTPS sur le
port 443, pour ne pas être distinguable, et donc être plus difficile
à filtrer.) Le RFC 7605 donne des détails sur
ce choix des ports.
Maintenant, quelles méthodes HTTP utiliser ? Le RFC exige que les
applications utilisant HTTP se servent uniquement des méthodes enregistrées,
comme GET
ou PUT
. Certes,
une procédure existe pour enregistrer de nouvelles méthodes mais
l'IETF
insiste que ces nouvelles méthodes doivent être génériques, et non
pas limitées aux besoins d'une seule application. (Le RFC 4791 avait créé des méthodes spécifiques, mais c'était
avant. C'est maintenant interdit.)
Donc, pas de méthodes nouvelles. Mais quelle(s) méthode(s)
utiliser ? GET
est le choix le plus
évident. Cette méthode est idempotente (et
permet donc, entre autres, la mémorisation), et a une sémantique
simple et compréhensible. Elle a quelques limites (comme le fait que
tous les éventuels paramètres doivent être dans l'URL, ce qui peut
nécessiter un encodage spécial, et peut empêcher des paramètres de
grande taille) mais rien de bien grave. Si c'est trop gênant pour
une application donnée, il ne reste plus qu'à utiliser
POST
.
Et pour récupérer des métadonnées sur le
service ? Le RFC note que la méthode OPTIONS
n'est pas très pratique, par exemple parce qu'elle ne permet pas de
donner comme documentation un simple URL (la méthode par défaut
étant GET
). Il recommande plutôt un URL dans
/.well-known
(RFC 8615),
en créant un nouveau nom, ou
bien avec les URL host-meta
(RFC 6415). Pour des métadonnées sur une ressource particulière,
il est recommandé d'utiliser les liens (RFC 8288). Le RFC
note que, dans ce dernier cas, l'en-tête Link:
marche même avec la méthode HEAD
donc pas
besoin de récupérer la ressource pour avoir des informations sur ses
métadonnées.
Et les codes de retour HTTP comme 403 ou 404, comment les utiliser ? D'abord, une application qui utilise HTTP n'a pas forcément un complet contrôle sur ces codes de retour, qui peuvent être générés par des composants logiciels différents. Donc, le client HTTP doit se méfier, le code reçu n'est pas forcément significatif de l'application. Ensuite, une application peut avoir davantage de messages différents qu'il n'existe de codes de retour HTTP, ce qui peut pousser à de mauvaises pratiques, comme l'utilisation de codes de retour non standard, ou commme l'utilisation de codes certes standard mais utilisés d'une manière très éloignée de ce qui était prévu. Bref, le RFC conseille de découpler les erreurs applicatives des erreurs HTTP, de ne pas chercher à tout prix un code de retour HTTP pour chaque erreur applicative, et de ne pas hésiter à utiliser les codes de retour génériques (comme 500, pour « quelque chose ne va pas dans le serveur »). Pour envoyer des informations plus détaillées sur l'erreur, il est préférable d'utiliser les techniques du RFC 7807. Autre avertissement du RFC, les raisons envoyées par le serveur après un code de retour (404 File not found) ne sont pas significatives. Dans certains cas (message HTTP dans un message HTTP), elles ne sont pas transmises du tout, contrairement au code de retour, la seule information sur laquelle on peut compter. L'application ne doit donc pas espérer que le client recevra ces raisons.
Autre difficulté pour le concepteur ou la conceptrice
d'applications utilisant HTTP, les redirections. Il y a quatre
redirections différentes en HTTP, chacune pouvant être temporaire ou
définitive, et permettant de changer de méthode ou pas (une requête
POST
indiquant une redirection suivie d'une
requête GET
par le client). On a donc :
Et il faut donc réfléchir un peu avant de choisir un code de redirection (le RFC privilégie 301 et 302, plus souples).
Et les champs dans l'en-tête ? Une application a souvent envie d'en ajouter, que ce soit dans la requête ou dans la réponse. Mais le RFC n'est pas très chaud, demandant qu'on mette les informations plutôt dans l'URL ou dans le corps du message HTTP. Si on crée de nouveaux en-têtes, en théorie (c'est très théorique…), il faut les enregistrer à l'IANA (RFC 9110, section 16.3). Si ces en-têtes ont une structure, il est très recommandé qu'elles suivent les règles du RFC 8941.
Et le corps du message, justement ? L'application doit spécifier quel format est attendu. C'est souvent JSON (RFC 8259) mais cela peut être aussi XML, CBOR (RFC 8949), etc.
Une des grandes forces d'HTTP est la possibilité de mémorisation,
décrite en détail dans le RFC 9111. La
mémorisation améliore les performances, rend le service moins
sensible aux perturbations, et permet le passage à
l'échelle. Les applications qui utilisent HTTP ont donc
tout intérêt à permettre et à utiliser cette mémorisation. Il est
donc recommandé d'indiquer dans la réponse une durée de vie, de
préférence avec Cache-Control: max-age=…
ou
bien, si c'est nécessaire, d'indiquer explicitement que la réponse
ne doit pas être mémorisée (Cache-Control:
no-store
).
Un autre avantage pour une application d'utiliser HTTP est l'existence d'un cadre général pour l'authentification (RFC 9110, section 11). Attention, certains mécanismes ne doivent être utilisés qu'au-dessus d'HTTPS, comme la basic authentication du RFC 7617. HTTPS permet également d'utiliser les certificats client pour l'authentification. (Attention, avec TLS ≤ 1.2, ces certificats, qui contiennent des données personnelles, sont transmis en clair.)
Conséquence de l'utilisation de HTTP, l'application est
utilisable via un navigateur Web. Cela peut
être vu comme un avantage (tout le monde a un navigateur Web sous la
main) ou comme un inconvénient (si la sémantique de l'application ne
permet pas réellement un usage pratique depuis un navigateur). Mais
quoi qu'on en pense, l'application sera accessible aux navigateurs,
et il est donc important de s'assurer que cela ne provoquera pas de
problème. Par exemple, si on peut changer un état avec une requête
POST
, l'application pourrait être attaquée
assez facilement par CSRF. Si l'application
tire une partie des données qu'elle renvoie en réponse d'une source
que l'attaquant peut contrôler, on risque le XSS. Il est donc recommandé,
même si l'application n'est pas prévue pour être utilisée par un
navigateur, de suivre les mêmes règles de développement sécurisé que
si elle devait être accédée depuis un navigateur, notamment :
X-Content-Type-Options: nosniff
,Referrer-Policy:
,HttpOnly
sur les
cookies (RFC 6265, section 5.2.6).Voici un exemple d'une réponse suivant ces principes :
HTTP/1.1 200 OK Content-Type: application/example+json X-Content-Type-Options: nosniff Content-Security-Policy: default-src 'none' Cache-Control: max-age=3600 Referrer-Policy: no-referrer
Il reste à régler la question des frontières de l'application. Le plus simple pour l'application est d'avoir son propre nom de domaine et donc une origine (RFC 6454) unique. Cela simplifie par exemple des problèmes comme les cookies. Mais cela complique le déploiement, empêchant de mettre plusieurs applications derrière le même nom. Le RFC conseille donc plutôt de concevoir des applications pouvant coexister avec d'autres applications sous le même nom (RFC 8820).
Un mot sur la sécurité pour finir (section 6
du RFC). D'abord, une application qui utilise HTTP va évidemment
hériter des questions de sécurité générale de HTTP, comme détaillées
dans la section 17 du RFC 9110. Vu le
caractère sensible des données traitées par beaucoup d'applications,
le RFC recommande l'utilisation de HTTPS. Mais il développe aussi
la question de la vie
privée. HTTP est très bavard et le serveur en apprend
beaucoup, souvent beaucoup trop, sur son client. Ainsi, les
cookies, l'adresse
IP source, les ETags
, les tickets
de session TLS sont très utiles au serveur qui voudrait
suivre un client. Et le RFC rappelle que HTTP donne assez
d'informations « auxiliaires » pour pouvoir reconnaitre un client
(ce qu'on nomme le fingerprinting). Bref, le
maintien de son intimité va être aussi difficile que sur le Web.
Ce RFC remplace l'ancien RFC 3205. Comme le note l'annexe A de notre RFC, il y a trop de changements pour les lister ; ce document est très différent de son prédécesseur (qui date de 2002 !).
Voyons maintenant quelques exemples d'application utilisant
HTTP. Dans le monde IETF, il y a évidemment
RDAP (RFC 7480, RFC 9082 et RFC 9083). RDAP
suit bien les principes de ce RFC. Par exemple, les chemins d'URL
comme /domain
ou /ip
ne
sont pas forcément à la « racine » du serveur HTTP. Autre exemple,
DoH (RFC 8484), également fidéle (heureusement !) aux
recommandations de l'IETF. Notez que ces recommandations laissent
des choix. Ainsi, lorsque le nom de domaine cherché n'est pas
trouvé, RDAP renvoie le code 404 (RFC 7480,
section 5.3) alors que DoH préfère renvoyer un 200 (le serveur HTTP
a bien été joignable et a bien répondu), gardant le signal de
non-existence uniquement dans la réponse DNS (RFC 8484,
section 4.2.1) transportée sur HTTP (l'argument est que DoH ne fait
que transporter les requêtes d'un autre protocole, contrairement à
RDAP).
J'ai parlé plus haut de la possibilité d'utilisation d'un
navigateur Web ordinaire pour accéder aux applications utilisant
HTTP. Mais comme ces applications envoient souvent des données
structurées en JSON, il faut un navigateur qui gère bien le
JSON. Et c'est justement ce que fait Firefox, qui sait l'afficher de manière
pratique :
Terminons avec quelques exemples d'API « finales » (donc pas le sujet
principal du RFC,
qui parle de protocoles IETF).
Commençons modestement par l'API du DNS
looking glass. A priori, elle suit tous les
principes de ce RFC. En tout cas, elle essaie. Mais si vous
constatez des différences avec le RFC, n'hésitez pas à faire un
rapport.
Autre API intéressante, celle des sondes RIPE Atlas. Elle utilise
toutes les possibilités de HTTP, notamment les multiples méthodes
(DELETE
pour supprimer une mesure en cours, par
exemple). J'aurais juste trouvé plus logique d'utiliser
PUT
au lieu de POST
pour
créer une mesure. L'API de Mastodon
(cf. sa
documentation) est
encore plus incohérente, utilisant POST
pour
créer un pouète, mais PUT
pour le mettre à jour.
Date de publication du RFC : Juin 2022
Auteur(s) du RFC : M. Nottingham (Fastly)
Chemin des normes
Réalisé dans le cadre du groupe de travail IETF httpbis
Première rédaction de cet article le 9 juin 2022
Ce RFC normalise enfin un en-tête HTTP pour permettre aux relais HTTP qui mémorisent les réponses reçues, les caches, d'indiquer au client comment ils ont traité une requête et si, par exemple, la réponse vient de la mémoire du relais ou bien s'il a fallu aller la récupérer sur le serveur HTTP d'origine.
Il existait déjà des en-têtes de débogage analogues mais
non-standards, variant aussi bien dans leur syntaxe que dans leur
sémantique. Le nouvel en-tête standard,
Cache-Status:
va, espérons-le, mettre de
l'ordre à ce sujet. Sa syntaxe suit les principes des en-têtes
structurés du RFC 8941. Sa valeur est donc une
liste de relais qui ont traité la requête, séparés par des
virgules, dans l'ordre des traitements. Le
premier relais indiqué dans un Cache-Status:
est donc le plus proche du serveur d'origine et le dernier relais
étant le plus proche de l'utilisateur. Voici un exemple avec un seul
relais, nommé cache.example.net
:
Cache-Status: cache.example.net; hit
et un exemple avec deux relais, OriginCache
puis CDN Company Here
:
Cache-Status: OriginCache; hit; ttl=1100, "CDN Company Here"; hit; ttl=545
La description complète de la syntaxe figure dans la section 2 de notre RFC. D'abord, il faut noter qu'un relais n'est pas obligé d'ajouter cet en-tête. Il le fait selon des critères qui lui sont propres. Le RFC déconseille fortement d'ajouter cet en-tête si la réponse a été générée localement par le relais (par exemple un 400 qui indique que le relais a détecté une requête invalide). En tout cas, le relais qui s'ajoute à la liste ne doit pas modifier les éventuels relais précédents. Chaque relais indique son nom, par exemple par un nom de domaine mais ce n'est pas obligatoire. Ce nom peut ensuite être suivi de paramètres, séparés par des point-virgules.
Dans le
premier exemple ci-dessus, hit
était un
paramètre du relais cache.example.net
. Sa
présence indique qu'une réponse était déjà mémorisée par le relais
et qu'elle a été utilisée (hit : succès, on a
trouvé), le serveur d'origine n'ayant pas été contacté. S'il y
avait une réponse stockée, mais qu'elle était rassise (RFC 9111, section 4.2) et qu'il a fallu la
revalider auprès du serveur d'origine, hit
ne
doit pas être utilisé.
Le deuxième paramètre possible est fwd
(pour
Forward) et il indique qu'il a fallu contacter le
serveur HTTP d'origine. Plusieurs raisons sont possibles pour cela,
entre autres :
bypass
, quand le relais a été configuré pour
que cette requête soit systématiquement relayée,method
parce que la méthode HTTP
utilisée doit être relayée (c'est typiquement le cas d'un
PUT
, cf. RFC 9110,
section 9.3.4),miss
, un manque, la ressource n'était
pas dans la mémoire du relais (notez qu'il existe aussi deux
paramètres plus spécifiques, uri-miss
et
vary-miss
),stale
, car la ressource était bien dans
la mémoire du relais mais, trop ancienne et rassise, a dû être revalidée (RFC 9111, section 4.3) auprès du
serveur d'origine.Ainsi, cet en-tête :
Cache-Status: ExampleCache; fwd=miss; stored
indique que l'information n'était pas dans la mémoire de
ExampleCache
et qu'il a donc dû faire suivre au
serveur d'origine (puis qu'il a stocké cette information dans sa
mémoire, le paramètre stored
).
Autre paramètre utile, ttl
, qui indique
pendant combien de temps l'information peut être gardée (cf. RFC 9111, section 4.2.1), selon les
calculs du relais. Quant à detail
,
il permet de donner des informations supplémentaires, de manière non
normalisée (la valeur de ce paramètre ne peut être interprétée que
si on connait le programme qui l'a produite).
Les paramètres sont enregistrés à l'IANA, dans un nouveau registre, et de nouveaux paramètres peuvent y être ajoutés, suivant la politique « Examen par un expert » du RFC 8126. La recommandation est de n'enregistrer que des paramètres d'usage général, non spécifiques à un programme particulier, sauf à nommer le paramètre d'une manière qui identifie clairement sa spécificité.
Questions mises en œuvre, Squid sait
renvoyer cet en-tête (cf. le code qui commence à SBuf
cacheStatus(uniqueHostname());
dans le fichier
src/client_side_reply.cc
). Ce code n'est pas
encore présent dans la version 5.2 de Squid, qui n'utilise encore que
d'anciens en-têtes non-standards, il faut attendre de nouvelles versions :
< X-Cache: MISS from myserver < X-Cache-Lookup: NONE from myserver:3128 < Via: 1.1 myserver (squid/5.2)
Je n'ai pas regardé les autres logiciels.
Date de publication du RFC : Juin 2022
Auteur(s) du RFC : E. Kinnear (Apple), P. McManus (Fastly), T. Pauly (Apple), T. Verma, C.A. Wood (Cloudflare)
Expérimental
Première rédaction de cet article le 9 juin 2022
Des protocoles de DNS sécurisés comme DoH protègent la liaison entre un client et un résolveur DNS. Mais le résolveur, lui, voit tout, aussi bien le nom de domaine demandé que l'adresse IP du client. Ce nouveau RFC décrit un mécanisme expérimental de relais entre le client et le serveur DoH, permettant de cacher l'adresse IP du client au serveur, et la requête au relais. Une sorte de Tor minimum.
Normalement, DoH (normalisé dans le RFC 8484) résout le problème de la surveillance des requêtes DNS. Sauf qu'évidemment le serveur DoH lui-même, le résolveur, voit le nom demandé, et la réponse faite. (Les utilisateurices de l'Internet oublient souvent que la cryptographie, par exemple avec TLS, protège certes contre le tiers qui écoute le réseau mais pas du tout contre le serveur avec qui on parle.) C'est certes nécessaire à son fonctionnement mais, comme il voit également l'adresse IP du client, il peut apprendre pas mal de choses sur le client. Une solution possible est d'utiliser DoH (ou DoT) sur Tor (exemple plus loin). Mais Tor est souvent lent, complexe et pas toujours disponible (cf. annexe A du RFC). D'où les recherches d'une solution plus légère, Oblivious DoH, décrit dans ce nouveau RFC. L'idée de base est de séparer le routage des messages (qui nécessite qu'on connaisse l'adresse IP du client) et la résolution DNS (qui nécessite qu'on connaisse la requête). Si ces deux fonctions sont assurées par deux machines indépendantes, on aura amélioré la protection de la vie privée.
Les deux machines vont donc être :
Un schéma simplifié :
Il est important de répéter que, si le relais et le serveur sont complices, Oblivious DoH perd tout intérêt. Ils doivent donc être gérés par des organisations différentes.
La découverte du relais, et de la clé publique du serveur, n'est pas traité dans ce RFC. Au moins au début, elle se fera « manuellement ».
Le gros morceau du RFC est sa section 4. Elle explique les
détails du protocole. Le client qui a une requête DNS à faire doit
la chiffrer avec la clé publique du serveur. Ce client a été
configuré avec un gabarit d'URI (RFC 6570) et avec
l'URL du
serveur DoH, serveur qui doit connaitre le mécanisme
Oblivious DoH. Le gabarit doit contenir deux
variables, targethost
(qui sera incarné avec le
nom du serveur DoH) et targetpath
(qui sera
incarné avec le chemin dans l'URL du serveur DoH). Par exemple, un
gabarit peut être
https://dnsproxy.example/{targethost}/{targetpath}
. Le
client va ensuite faire une requête (méthode HTTP
POST
) vers le relais. La requête aura le
type application/oblivious-dns-message
(la réponse également, donc le client a intérêt à indiquer
Accept: application/oblivious-dns-message
).
En supposant un serveur DoH d'URL
https://dnstarget.example/dns
, la requête sera
donc (en utilisant la syntaxe de description de HTTP/2 et 3) :
:method = POST :scheme = https :authority = dnsproxy.example :path = /dnstarget.example/dns accept = application/oblivious-dns-message content-type = application/oblivious-dns-message content-length = 106 [La requête, chiffrée]
Le relais enverra alors au serveur :
:method = POST :scheme = https :authority = dnstarget.example :path = /dns accept = application/oblivious-dns-message content-type = application/oblivious-dns-message content-length = 106 [La requête, chiffrée]
Le relais ne doit pas ajouter quelque chose qui permettrait
d'identifier le client, ce qui annulerait tout l'intérêt
d'Oblivious DoH. Ainsi, le champ
forwarded
(RFC 7239) est
interdit. Et bien sûr, on n'envoie pas de
cookies, non plus.
La réponse utilise le même type de médias :
:status = 200 content-type = application/oblivious-dns-message content-length = 154 [La réponse, chiffrée]
Le relais, en la transmettant, ajoutera un champ
proxy-status
(RFC 9209).
Les messages de type
application/oblivious-dns-message
sont encodés
ainsi (en utilisant le langage de description de TLS, cf. RFC 8446, section 3) :
struct { opaque dns_message<1..2^16-1>; opaque padding<0..2^16-1>; } ObliviousDoHMessagePlaintext;
Bref, un message DNS binaire, et du remplissage. Le remplissage est nécessaire car TLS, par défaut, n'empêche pas l'analyse de trafic. (Lisez le RFC 8467.) Le tout est ensuite chiffré puis mis dans :
struct { uint8 message_type; opaque key_id<0..2^16-1>; opaque encrypted_message<1..2^16-1>; } ObliviousDoHMessage;
Le key_id
servant à identifier ensuite le
matériel cryptographique utilisé.
J'ai parlé à plusieurs reprises de chiffrement. Le client doit connaitre la clé publique du serveur DoH pour pouvoir chiffrer. (Le RFC ne prévoit pas de mécanisme pour cela.) Comme avec la plupart des systèmes de chiffrement, le chiffrement sera en fait réalisé avec un algorithme symétrique. La transmission ou la génération de la clé de cet algorithme utlisera le mécanisme HPKE (Hybrid Public Key Encryption, spécifié dans le RFC 9180).
Outre l'analyse de trafic, citée plus haut, une potentielle faille d'Oblivious DoH serait le cas d'un serveur indiscret qui essaierait de déduire des informations de l'établissement des connexions DoH vers lui. Le RFC recommande que les relais établissent des connexions de longue durée et les réutilisent pour plusieurs clients, rendant cette éventuelle analyse plus complexe.
Notez aussi que le relais est évidemment libre de sa politique. Les relais peuvent par exemple ne relayer que vers certains serveurs DoH connus.
Le serveur DoH ne connait pas l'adresse IP du client (c'est le but) et ne peut donc pas transmettre aux serveurs faisant autorité les informations qui peuvent leur permettre d'envoyer une réponse adaptée au client final. Si c'est un problème, le client final peut toujours utiliser ECS (EDNS Client Subnet, RFC 7871), en indiquant un préfixe IP assez générique pour ne pas trop en révéler. Mais cela fait évidemment perdre une partie de l'intérêt d'Oblivious DoH.
Bon, et questions mises en œuvre de ce RFC ? Il y en a une dans DNScrypt (et ils publient une liste de serveurs et de relais).
Oblivious DoH est en travaux depuis un certain temps (voyez par exemple cet article de Cloudflare). Notez que la technique de ce RFC est spécifique à DoH. Il y a aussi des travaux en cours à l'IETF pour un mécanisme plus général, comme par exemple un Oblivious HTTP, qui serait un concurrent « low cost » de Tor.
À propos de Tor, j'avais dit plus haut qu'on pouvait évidemment, si on ne veut pas révéler son adresse IP au résolveur DoH, faire du DoH sur Tor. Voici un exemple de requête DoH faite avec le client kdig et le programme torsocks pour faire passer les requêtes TCP par Tor :
% torsocks kdig +https=/ @doh.bortzmeyer.fr ukraine.ua ;; TLS session (TLS1.3)-(ECDHE-SECP256R1)-(RSA-PSS-RSAE-SHA256)-(AES-256-GCM) ;; HTTP session (HTTP/2-POST)-(doh.bortzmeyer.fr/)-(status: 200) ... ;; ANSWER SECTION: ukraine.ua. 260 IN A 104.18.7.16 ukraine.ua. 260 IN A 104.18.6.16 ... ;; Received 468 B ;; Time 2022-06-02 18:45:15 UTC ;; From 193.70.85.11@443(TCP) in 313.9 ms
Et ce résolveur DoH ayant un service
.onion
, on peut même :
% torsocks kdig +https=/ @lani4a4fr33kqqjeiy3qubhfx2jewfd3aeaepuwzxrx6zywp2mo4cjad.onion ukraine.ua ;; TLS session (TLS1.3)-(ECDHE-SECP256R1)-(RSA-PSS-RSAE-SHA256)-(AES-256-GCM) ;; HTTP session (HTTP/2-POST)-(lani4a4fr33kqqjeiy3qubhfx2jewfd3aeaepuwzxrx6zywp2mo4cjad.onion/)-(status: 200) ... ;; ANSWER SECTION: ukraine.ua. 212 IN A 104.18.6.16 ukraine.ua. 212 IN A 104.18.7.16 ... ;; Received 468 B ;; Time 2022-06-02 18:46:00 UTC ;; From 127.42.42.0@443(TCP) in 1253.8 ms
Pour comparer les performances (on a dit que la latence de Tor était franchement élevée), voici une requête non-Tor vers le même résolveur :
% kdig +https=/ @doh.bortzmeyer.fr ukraine.ua ;; TLS session (TLS1.3)-(ECDHE-SECP256R1)-(RSA-PSS-RSAE-SHA256)-(AES-256-GCM) ;; HTTP session (HTTP/2-POST)-([2001:41d0:302:2200::180]/)-(status: 200) ... ;; ANSWER SECTION: ukraine.ua. 300 IN A 104.18.6.16 ukraine.ua. 300 IN A 104.18.7.16 ... ;; Received 468 B ;; Time 2022-06-02 18:49:44 UTC ;; From 2001:41d0:302:2200::180@443(TCP) in 26.0 ms
Date de publication du RFC : Juin 2022
Auteur(s) du RFC : M. Thomson (Mozilla), C. Benfield (Apple)
Chemin des normes
Réalisé dans le cadre du groupe de travail IETF httpbis
Première rédaction de cet article le 7 juin 2022
Le protocole HTTP, à la base des échanges sur le Web, a plusieurs versions, 1, 2 et 3. Toutes ont en commun la même sémantique, décrite dans le RFC 9110. Mais l'encodage sur le câble est différent. HTTP/2 a un encodage binaire et un transport spécifique (binaire, multiplexé, avec possibilité de push). Fini de déboguer des serveurs HTTP avec telnet. En échange, cette version promet d'être plus rapide, notamment en diminuant la latence lors des échanges. Ce RFC remplace l'ancienne norme HTTP/2 (RFC 7540), mais le protocole ne change pas, il s'agit surtout d'une réécriture pour suivre le nouveau cadre de normalisation, où un RFC générique, le RFC 9110 spécifie la sémantique de HTTP et où il y a un RFC spécifique par version.
La section 1 de notre RFC résume les motivations derrère cette version 2, et notamment les limites de HTTP/1.1, normalisé dans le RFC 9112 :
Au contraire, HTTP/2 n'utilise toujours qu'une seule connexion TCP, de longue durée (ce qui sera plus sympa pour le réseau). L'encodage étant entièrement binaire, le traitement par le récepteur est normalement plus rapide. Le RFC note toutefois que HTTP/2, qui dépend de TCP et n'utilise qu'une seule connexion TCP, ne résout pas le problème du head-of-line blocking. (Pour cela, il faudrait HTTP/3, normalisé dans le RFC 9114.)
La section 2 de notre RFC résume l'essentiel de ce qu'il faut
savoir sur HTTP/2. Il garde la sémantique générale de HTTP (donc,
par exemple, un GET
de
/cetteressourcenexistepas
fait un 404). La
norme HTTP/2 ne normalise que le transport des messages, pas les
messages ou leurs réponses (qui sont décrits par le RFC 9110). Notez que HTTP/2 s'appelait il y a très longtemps
SPDY (initialement lancé par
Google).
Avec HTTP/2, l'unité de base de la communication est la
trame (frame, et, dans
HTTP/2, vous pouvez oublier la définition traditionnelle qui en fait
l'équivalent du paquet, mais pour la couche 2). Chaque trame a un type et, par exemple,
les échanges HTTP traditionnels se feront avec simplement une trame
HEADERS
en requête et une
DATA
en réponse. Certains types sont
spécifiques aux nouvelles fonctions de HTTP/2, comme
SETTINGS
ou
PUSH_PROMISE
.
Les trames voyagent ensuite dans des ruisseaux
(streams), chaque ruisseau hébergeant un et un
seul échange requête/réponse. On crée donc un ruisseau à chaque fois
qu'on a un nouveau GET
ou
POST
à faire. Les petits ruisseaux sont ensuite
multiplexés dans une grande rivière, l'unique connexion TCP entre un
client HTTP et un serveur. Les ruisseaux ont des mécanismes de
contrôle du trafic (ils avaient aussi un mécanisme de prioritisation
entre eux, que notre RFC abandonne).
Les en-têtes sont comprimés, en favorisant le cas le plus courant, de manière à s'assurer, par exemple, que la plupart des requêtes HTTP tiennent dans un seul paquet de la taille des paquets Ethernet.
Bon, maintenant, les détails pratiques (le RFC fait 90
pages). D'abord, l'établissement de la connexion. HTTP/2 tourne
au-dessus de TCP. Comment on fait pour savoir si le serveur
accepte HTTP/2 ? C'est marqué dans l'URL ? Non, les URL sont les mêmes,
avec les plans http:
et
https:
. On utilise un nouveau port, succédant au 80 de HTTP ?
Non. Les ports sont les mêmes, 80 et 443. On regarde dans le
DNS ou ailleurs si
le serveur sait faire du HTTP/2 ? Pas encore, bien que le futur
type de données DNS HTTPS
permettra cela. Aujourd'hui, les
méthodes pour savoir si le client doit tenter HTTP/2 sont :
--http2
de curl dans les exemples plus loin),Alt-Svc:
du RFC 7838, qui nécessite de tenter en HTTP/1 d'abord,h2
(HTTP/2 sur TLS). Le serveur, recevant
l'extension ALPN avec h2
, saura ainsi qu'on
fait du HTTP/2 et on pourra tout de suite commencer l'échange de
trames HTTP/2. (h2
est dans le
registre IANA. Dans le RFC 7540, il y
avait aussi un h2c
dont l'usage est
maintenant abandonné.)Upgrade:
qui était dans la
section 6.7 du RFC 7230 est désormais
abandonné.Une fois qu'un client aura réussi à établir une connexion avec un serveur en HTTP/2, il sait que le serveur gère ce protocole. Il peut s'en souvenir, pour les futures connexions (mais attention, ce n'est pas une indication parfaite : un serveur peut abandonner HTTP/2, par exemple).
Maintenant, c'est parti, on s'envoie des trames (il y a d'abord
une préface, un nombre magique qui permet de
s'assurer que tout le monde comprend bien HTTP/2, mais je n'en
parlerai pas davantage). À quoi
ressemblent ces trames (section 4) ? Elles commencent par un en-tête
indiquant leur longueur, leur type (comme
SETTINGS
, HEADERS
,
DATA
… cf. section 6), des options (comme
ACK
qui sert aux trames de type
PING
à distinguer requête et réponse) et
l'identificateur du ruisseau auquel la trame appartient (un nombre
sur 31 bits). Le format complet est en section 4.1.
Les en-têtes HTTP sont comprimés selon la méthode normalisée dans le RFC 7541.
Les ruisseaux (streams), maintenant. Ce sont donc des suites ordonnées de trames, bi-directionnelles, à l'intérieur d'une connexion HTTP/2. Une connexion peut comporter plusieurs ruisseaux, chacun identifié par un stream ID (un entier de quatre octets, pair si le ruisseau a été créé par le serveur et impair autrement). Les ruisseaux sont ouverts et fermés dynamiquement et leur durée de vie n'est donc pas celle de la connexion HTTP/2. Contrairement à TCP, il n'y a pas de « triple poignée de mains » : l'ouverture d'un ruisseau est unilatérale et peut donc se faire très vite (rappelez-vous que chaque échange HTTP requête/réponse nécessite un ruisseau qui lui est propre ; pour vraiment diminuer la latence, il faut que leur création soit rapide). Les identificateurs ne sont jamais réutilisés (si on tombe à cours, la seule solution est de fermer la connexion TCP et d'en ouvrir une autre).
Un mécanisme de contrôle du flot s'assure que les ruisseaux se
partagent pacifiquement la connexion. C'est donc une sorte de TCP
dans le TCP, réinventé pour les besoins de HTTP/2 (section 5.2 et
relire aussi le RFC 1323). Le récepteur
indique (dans une trame WINDOWS_UPDATE
) combien
d'octets il est prêt à recevoir (64 Kio par défaut) et l'émetteur
s'arrête dès qu'il a rempli cette fenêtre d'envoi. (Plus exactement,
s'arrête d'envoyer des trames DATA
: les
autres, les trames de contrôle, ne sont pas soumises au contrôle du
flot).
Comme si ce système des connexions dans les connexions n'était pas assez compliqué comme cela, il y a aussi des dépendances entre ruisseaux. Un ruisseau peut indiquer qu'il dépend d'un autre et, dans ce cas, les ressources seront allouées d'abord au ruisseau dont on dépend. Par exemple, le code JavaScript ne peut en général commencer à s'exécuter que quand toute la page est chargée, et on peut donc le demander dans un ruisseau dépendant de celle qui sert à charger la page. On peut dépendre d'un ruisseau dépendant, formant ainsi un arbre de dépendances.
Il peut bien sûr y avoir des erreurs dans la
communication. Certaines affectent toute la connexion, qui devra
être abandonnée, mais d'autres ne concernent qu'un seul
ruisseau. Dans le premier cas, celui qui détecte l'erreur envoie une
trame GOAWAY
(dont on ne peut pas garantir
qu'elle sera reçue, puisqu'il y a une erreur) puis coupe la
connexion TCP. Dans le second cas, si le problème ne concerne qu'un
seul ruisseau, on envoie la trame RST_STREAM
qui arrête le traitement du ruisseau.
HTTP/2 avait (RFC 7540, section 5.3) un mécanisme de priorité entre trames, qui permettait d'éviter, par exemple, que la récupération d'une grosse image ne ralentisse le chargement d'une feuille de style. Mais il était trop complexe, et a été peu mis en œuvre, la plupart des serveurs ignoraient les demandes de priorité des clients. Un nouveau mécanisme est décrit dans le RFC 9218.
Notre section 5 se termine avec des règles qui indiquent comment gérer des choses inconnues dans le dialogue. Ces règles permettent d'étendre HTTP/2, en s'assurant que les vieilles mises en œuvre ne pousseront pas des hurlements devant les nouveaux éléments qui circulent. Par exemple, les trames d'un type inconnu doivent être ignorées et mises à la poubelle directement, sans protestation.
On a déjà parlé plusieurs fois des trames, la section 6 du RFC détaille leur définition. Ce sont aux ruisseaux ce que les paquets sont à IP et les segments à TCP. Les trames ont un type (un entier d'un octet). Les types possibles sont enregistrés à l'IANA. Les principaux types actuels sont :
DATA
(type 0), les trames les plus
nombreuses, celles qui portent les données, comme les pages
HTML (elles
peuvent aussi contenir du
remplissage, pour éviter qu'un observateur ne
déduise de la taille des réponses la page qu'on regardait,
cf. section 10.7),HEADERS
(type 1), qui portent les
en-têtes HTTP, dûment comprimés selon le RFC 7541,PRIORITY
(type 2) indiquait la priorité que
l'émetteur donne au ruisseau qui porte cette trame, mais ce type de trame
n'est désormais plus utilisé,RST_STREAM
(type 3), dont j'ai parlé plus
haut à propos des erreurs, permet de terminer un ruisseau (filant la
métaphore, on pourrait dire que cela assèche le ruisseau ?),SETTINGS
(type 4), permet d'envoyer des
paramètres, comme SETTINGS_HEADER_TABLE_SIZE
,
la taille de la table utilisée pour la compression des en-têtes,
SETTINGS_MAX_CONCURRENT_STREAMS
pour indiquer
combien de ruisseaux est-on prêt à gérer, etc (la liste des
paramètres est dans un
registre IANA),PUSH_PROMISE
(type 5) qui indique qu'on
va transmettre des données non sollicitées
(push), du moins si le paramètre
SETTINGS_ENABLE_PUSH
est à 1,PING
(type 6) qui permet de tester le
ruisseau (le partenaire va répondre avec une autre trame
PING
, ayant l'option ACK
à
1),GOAWAY
(type 7) que nous avons déjà vu
plus haut, sert à mettre fin proprement (le pair est informé de ce
qui va se passer) à une connexion,WINDOW_UPDATE
(type 8) sert à faire
varier la taille de la fenêtre (le nombre d'octets qu'on peut encore
accepter, cf. section 6.9.1),CONTINUATION
(type 9), indique la suite
d'une trame précédente. Cela n'a de sens que pour certains types
comme HEADERS
(ils peuvent ne pas tenir dans
une seule trame) ou CONTINUATION
lui-même. Mais
une trame CONTINUATION
ne peut pas être
précédée de DATA
ou de
PING
, par exemple.Dans le cas vu plus haut d'erreur entrainant la fin d'un ruisseau
ou d'une connexion entière, il est nécessaire d'indiquer à son
partenaire en quoi consistait l'erreur en question. C'est le rôle
des codes d'erreur de la section 7. Stockés sur quatre octets (et
enregistrés dans un
registre IANA), ils sont transportés par les trames
RST_STREAM
ou GOAWAY
qui
terminent, respectivement, ruisseaux et connexions. Parmi ces
codes :
NO_ERROR
(code 0), pour les cas de
terminaison normale,PROTOCOL_ERROR
(code 1) pour ceux où le
pair a violé une des règles de HTTP/2, par exemple en envoyant une
trame CONTINUATION
qui n'était pas précédée de
HEADERS
, PUSH_PROMISE
ou
CONTINUATION
,INTERNAL_ERROR
(code 2), un malheur est
arrivé,ENHANCE_YOUR_CALM
(code 11), qui ravira
les amateurs de spam et de
Viagra, demande au partenaire en face de se
calmer un peu, et d'envoyer moins de requêtes.Toute cette histoire de ruisseaux, de trames, d'en-têtes
comprimés et autres choses qui n'existaient pas en HTTP/1 est bien
jolie mais HTTP/2 n'a pas été conçu comme un remplacement de TCP,
mais comme un moyen de faire passer des dialogues HTTP. Comment met-on
les traditionnelles requêtes/réponses HTTP sur une connexion
HTTP/2 ? La section 8 répond à cette question. D'abord, il faut se
rappeler que HTTP/2 est du HTTP. La sémantique est donc celle du
RFC 9110. Il y a quelques différences comme le
fait que certains en-têtes disparaissent, par exemple
Connection:
(section 8.2.2) qui n'est plus
utile en HTTP/2 ou Upgrade:
(section 8.6).
HTTP est requête/réponse. Pour envoyer une requête, on utilise un
nouveau ruisseau (envoi d'une trame avec un numéro de ruisseau non
utilisé), sur laquelle on lira la réponse (les ruisseaux ne sont pas
persistents). Dans le cas le plus fréquent, la requête sera composée
d'une trame HEADERS
contenant les en-têtes
(comme User-Agent:
ou
Host:
, cf. RFC 9110,
section 10.1) et les « pseudo-en-têtes » comme la méthode
(GET
, POST
, etc), avec
parfois des trames DATA
(cas d'un
POST
). La réponse comprendra une trame
HEADERS
avec les en-têtes (comme
Content-Length:
) et les pseudo-en-têtes comme
le code de retour HTTP (200, 403, 500, etc) suivie de plusieurs
trames DATA
contenant les données (HTML, CSS, images,
etc). Des variantes sont possibles (par exemple, les trames
HEADERS
peuvent être suivies de trames
CONTINUATION
). Les en-têtes ne sont pas
transportés sous forme texte (ce qui était le cas en HTTP/1, où on
pouvait utiliser telnet comme client HTTP)
mais encodés en binaire, et comprimés selon
le RFC 7541. À noter que cet encodage implique
une mise du nom de l'en-tête en minuscules.
J'ai parlé plus haut des pseudo-en-têtes : c'est le mécanisme
HTTP/2 pour traiter des informations qui ne sont pas des en-têtes
en HTTP (section 8.3). Ces informations sont mises dans les
HEADERS
HTTP/2, précédés d'un
deux-points. C'est le cas de la méthode (RFC 9110, section 9.3), donc GET
sera encodé :method GET
. L'URL sera éclaté dans
les pseudo-en-têtes :scheme
,
:path
, etc. Idem pour la réponse HTTP, le
fameux code à trois lettres est désormais un pseudo-en-tête,
:status
.
Le RFC met en garde les programmeur·ses : certains caractères peuvent être dangereux car profitant des faiblesses de certains analyseurs ou bien utilisant le fait que HTTP n'est pas toujours de bout en bout et qu'un message peut être traduit de HTTP/1 en HTTP/2 (ou réciproquement). Un deux-points dans le nom d'un champ, par exemple, pourrait produire un message dont l'interprétation ne serait pas celle attendue (ce qu'on nomme le request smuggling).
Voici des exemples de requêtes HTTP (mais vous ne le verrez pas ainsi si vous espionnez le réseau, en raison de la compression du RFC 7541) :
### HTTP/1, pas de corps dans la requête ### GET /resource HTTP/1.1 Host: example.org Accept: image/jpeg ### HTTP/2 (une trame HEADERS) :method = GET :scheme = https :path = /resource host = example.org accept = image/jpeg
Puis une réponse qui n'a pas de corps :
### HTTP/1 ### HTTP/1.1 304 Not Modified ETag: "xyzzy" Expires: Thu, 23 Jan ... ### HTTP/2, une trame HEADERS ### :status = 304 etag = "xyzzy" expires = Thu, 23 Jan ...
Une réponse plus traditionnelle, qui inclut un corps :
### HTTP/1 ### HTTP/1.1 200 OK Content-Type: image/jpeg Content-Length: 123 {binary data} ### HTTP/2 ### # trame HEADERS :status = 200 content-type = image/jpeg content-length = 123 # trame DATA {binary data}
Plus compliqué, un cas où les en-têtes de la requête ont été mis dans deux trames, et où il y avait un corps dans la requête :
### HTTP/1 ### POST /resource HTTP/1.1 Host: example.org Content-Type: image/jpeg Content-Length: 123 {binary data} ### HTTP/2 ### # trame HEADERS :method = POST :path = /resource :scheme = https # trame CONTINUATION content-type = image/jpeg host = example.org content-length = 123 # trame DATA {binary data}
Nouveauté introduite par HTTP/2, la possibilité pour le serveur
de pousser (push, section 8.4 de notre RFC) du
contenu non sollicité vers le client (sauf si cette possibilité a
été coupée par le paramètre
SETTINGS_ENABLE_PUSH
). Pour cela, le serveur
(et lui seul) envoie une trame de type
PUSH_PROMISE
au client, en utilisant le
ruisseau où le client avait fait une demande originale (donc, la
sémantique de PUSH_PROMISE
est « je te promets
que lorsque le moment sera venu, je répondrai plus longuement à ta
question »). Cette trame contient une requête HTTP. Plus tard,
lorsque le temps sera venu, le serveur tiendra sa promesse en
envoyant la « réponse » de cette « requête » sur le ruisseau qu'il
avait indiqué dans le PUSH_PROMISE
.
Et enfin, à propos des méthodes HTTP/1 et de leur équivalent en
HTTP/2, est-ce que CONNECT
(RFC 9110, section 9.3.6) fonctionne toujours ? Oui, on peut
l'utiliser pour un tunnel sur un
ruisseau. (Un tunnel sur un ruisseau... Beau défi pour le
génie civil.)
La section 9 de notre RFC rassemble quelques points divers. Elle rappelle que, contrairement à HTTP/1, toutes les connexions sont persistentes et que le client n'est pas censé les fermer avant d'être certain qu'il n'en a plus besoin. Tout doit passer à travers une connexion vers le serveur et les clients ne doivent plus utiliser le truc d'ouvrir plusieurs connexions HTTP avec le serveur. De même, le serveur laisse les connexions ouvertes le plus longtemps possible, mais a le droit de les fermer s'il doit économiser des ressources.
À noter qu'on peut utiliser une connexion prévue pour un autre
nom, du moment que cela arrive au même serveur (même adresse IP). Le
pseudo-en-tête :authority
sert à départager les
requêtes allant à chacun des serveurs. Mais attention si la session
utilise TLS !
L'utilisation d'une connexion avec un autre
:authority
(host + port)
n'est possible que si le certificat serveur qui a été utilisé est
valable pour tous (par le biais des
subjectAltName
, ou bien d'un joker).
À propos de TLS, la section
9.2 prévoit quelques règles qui n'existaient pas en HTTP/1 (et dont
la violation peut entrainer la coupure de la connexion avec l'erreur
INADEQUATE_SECURITY
) :
Puisqu'on parle de sécurité, la section 10 traite un certain nombre de problèmes de sécurité de HTTP/2. Elle rappelle que TLS est fortement recommandé (mais il n'est devenu obligatoire qu'avec HTTP/3, HTTP/2 permettant toutefois une utilisation « opportuniste », cf. RFC 8164). Parmi les problèmes qui sont spécifiques à HTTP/2, on note que ce protocole demande plus de ressources que HTTP/1, ne serait-ce que parce qu'il faut maintenir un état pour la compression. Il y a donc potentiellement un risque d'attaque par déni de service. Une mise en œuvre prudente veillera donc à limiter les ressources allouées à chaque connexion.
Enfin, il y a la question de la vie privée, un sujet chaud dans le monde HTTP depuis longtemps. Les options spécifiques à HTTP/2 (changement de paramètres, gestion du contrôle de flot, traitement des innombrables variantes du protocole) peuvent permettre d'identifier une machine donnée par son comportement. HTTP/2 facilite donc le fingerprinting.
En outre, comme une seule connexion TCP est utilisée pour toute une visite sur un site donné, cela peut rendre explicite une information comme « le temps passé sur un site », information qui était implicite en HTTP/1, et qui devait être reconstruite.
Comme on le voit, HTTP/2 est bien plus complexe que HTTP/1. On ne peut pas espérer programmer un client ou un serveur en quelques heures, comme on le fait avec HTTP/1. C'est en partie pour cela que personne ne prévoit un abandon de HTTP/1, qui continuera à coexister avec HTTP/2 (et HTTP/3 !) pendant très longtemps encore.
Question mises en œuvre, HTTP/2 est désormais présent dans la quasi-totalité des clients, serveurs et bibliothèques HTTP. Ici, avec curl, en forçant l'utilisation de HTTP/2 dès le début :
% curl -v --http2 https://www.bortzmeyer.org/7540.html * Trying 2001:4b98:dc0:41:216:3eff:fe27:3d3f:443... * Connected to www.bortzmeyer.org (2001:4b98:dc0:41:216:3eff:fe27:3d3f) port 443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 ... * ALPN, server accepted to use h2 * Server certificate: * subject: CN=www.bortzmeyer.org ... * Using HTTP2, server supports multiplexing * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0 * h2h3 [:method: GET] * h2h3 [:path: /7540.html] * h2h3 [:scheme: https] * h2h3 [:authority: www.bortzmeyer.org] * h2h3 [user-agent: curl/7.82.0] * h2h3 [accept: */*] * Using Stream ID: 1 (easy handle 0x5639a5d10cf0) > GET /7540.html HTTP/2 > Host: www.bortzmeyer.org > user-agent: curl/7.82.0 > accept: */* ... < HTTP/2 200 ... < etag: "b4b0-5de09d5830d11" < content-type: text/html; charset=UTF-8 < date: Thu, 05 May 2022 13:07:08 GMT < server: Apache/2.4.53 (Debian) < <?xml version="1.0" ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xml:lang="fr" lang="fr" xmlns="http://www.w3.org/1999/xhtml"> <head> ...
Vous voulez voir un joli pcap de HTTP/2 ? La plupart des sites accessibles en HTTP/2 (et parfois des clients) imposent TLS. Il faut donc, si on veut voir « à l'intérieur » des paquets, utiliser la technique classique d'exportation de la clé :
% export SSLKEYLOGFILE=/tmp/http2.key % curl --http2 https://www.bortzmeyer.org/7540.html
Puis vérifiez que dans les préférences de
Wireshark, on a cette clé (par exemple dans
~/.config/wireshark/preferences
, une ligne
tls.keylog_file: /tmp/http2.key
). On peut alors
regarder le pcap en détail. Voici un tel
pcap et la clé
correspondante. Cela permet de regarder le contenu des
messages avec Wireshark :
Cela permet aussi, avec une commande comme
tshark -V -r http2.pcap > http2.txt
, de
produire un joli fichier
d'analyse. Notez les identificateurs de ruisseaux
(Stream ID) : il n'y en a que deux, 0 et 1, car
on n'a chargé qu'une ressource (il y a un ruisseau dans chaque direction). Je
vous laisse faire vous-même l'opération pour le cas de deux
ressources, avec une commande comme curl -v --http2
https://www.bortzmeyer.org/7540.html
https://www.bortzmeyer.org/9116.html
. Vous verrez alors
le parallélisme de HTTP/2 et les multiples ruisseaux.
Et, sinon, si vous voulez activer HTTP/2 sur un serveur
Apache, c'est aussi
simple que de charger le module http2
et de
configurer :
Protocols h2 http/1.1
Sur Debian, la commande a2enmod
http2
fait tout cela automatiquement. Pour vérifier que
cela a bien été fait, vous pouvez utiliser curl
-v
comme vu plus haut, ou bien un site de test (comme
KeyCDN) ou
encore la fonction Inspect element (clic droit
sur la page, puis onglet Network puis
sélectionner une des ressources chargées) de Firefox :
L'annexe B liste les principaux changements depuis le RFC 7540, notamment :
Upgrade:
pour passer de HTTP/1 en HTTP/2,Et, sinon, si vous voulez vous instruire sur HTTP/2 sans lire tout le RFC, il y a évidemment le livre de Daniel Stenberg.
Date de publication du RFC : Juin 2022
Auteur(s) du RFC : C. Krasic (Netflix), M. Bishop (Akamai Technologies), A. Frindell (Facebook)
Chemin des normes
Réalisé dans le cadre du groupe de travail IETF quic
Première rédaction de cet article le 7 juin 2022
QPACK, normalisé dans ce RFC est un mécanime de compression des en-têtes HTTP, prévu spécifiquement pour HTTP/3. Il est proche du HPACK de HTTP/2, mais adapté aux particularités de QUIC.
Car le HPACK du RFC 7541 a un défaut qui n'était pas un problème en HTTP/2 mais le devient avec HTTP/3 : il supposait que les trames arrivent dans l'ordre d'émission, même si elles circulent sur des ruisseaux différents. Ce n'est plus vrai en HTTP/3 qui, grâce à son transport sous-jacent, QUIC, a davantage de parallélisme, et où une trame peut en doubler une autre (si elles n'étaient pas dans le même ruisseau). QPACK ressemble à HPACK, mais en ayant corrigé ce problème. (Au fait, ne cherchez pas ce que veut dire QPACK, ce n'est pas un acronyme.)
Donc, le principe de QPACK. Comme HPACK, on travaille avec deux
tables, qui vont associer aux en-têtes HTTP un nombre (appelé
index). L'une des tables est statique, définie dans ce RFC (annexe
A) et donc identique pour tout le monde. L'autre est dynamique et
construite par un échange de messages transmis dans des ruisseaux
QUIC. Le fonctionnement avec la table statique est simple :
l'encodeur regarde si ce qu'il veut écrire est dans la table, si
oui, il le remplace par l'index. Le décodeur, recevant un index, le
remplace par le contenu de la table. Par exemple, l'en-tête HTTP
if-none-match
(RFC 7232,
section 3.2) est dans la table, index 9. L'encodeur remplacera donc
un if-none-match
par 9 (12 octets de gagnés si
tout était sous forme de caractères de 8 bits, mais peut-être un peu
plus ou un peu moins, avec l'encodage de QPACK), et le décodeur fera
l'inverse.
J'ai un peu simplifié en supposant que la table ne contenait que
les noms des en-têtes. Elle peut aussi contenir leur valeur si
celle-ci est très courante. Ainsi, accept:
application/dns-message
est dans la table, index 30, vu
son utilisation intensive par DoH (RFC 8484). Même chose
pour content-type: text/html;charset=utf-8
à
l'index 52.
La table dynamique est évidemment bien plus complexe. Encodeur (celui qui comprime) et décodeur doivent cette fois partager un état. En outre, le parallélisme inhérent à QUIC fait qu'un message d'ajout d'une entrée dans la table pourrait arriver après le message utilisant cette entrée. QPACK fonctionne de la façon suivante :
QUIC a un système de contrôle de flux, et des inconvénients peuvent en résulter, par exemple un blocage des messages de contrôle de la table. Pour éviter tout blocage, un encodeur peut n'utiliser que des entrées de la table qui ont déjà fait l'objet d'un accusé de réception. Il comprimera moins mais ne risquera pas d'être bloqué.
Le RFC détaille les précautions à prendre pour éviter l'interblocage. Ainsi, les messages modifiant la table risquent d'être bloqués par ce système alors que le récepteur n'autorise pas de nouvelles trames tant qu'il n'a pas traité des trames qui ont besoin de ces nouvelles entrées dans la table dynamique. L'encodeur a donc pour consigne de ne pas tenter de modifier la table s'il ne lui reste plus beaucoup de « crédits » d'envoi de données. (D'une manière générale, quand il y a des choses compliquées à faire, QPACK demande à l'encodeur de les faire, le décodeur restant plus simple.)
L'encodage des messages QPACK est spécifié dans la section 4. QPACK utilise deux ruisseaux QUIC unidirectionnels, un dans chaque direction. Ils sont enregistrés à l'IANA.
Notez aussi qu'il y a deux instructions d'ajout d'une entrée dans
la table dynamique, une qui ajoute une valeur litérale (comme « ajoute
accept-language: fr
») et une qui ajoute une
valeur exprimée sous forme d'une référence à une entrée d'une table
(qui peut être la statique ou la dynamique). Par exemple, comme
accept-language
est dans la table statique, à
l'index 72, on
peut dire simplement « ajoute 72: fr ». Encore quelques octets
gagnés.
Dans la trame SETTINGS
de HTTP/3, deux
paramètres concernent spécialement QPACK, pour indiquer la taille
maximale de la table dynamique, et le nombre maximal de ruisseaux
bloqués. Ils sont placés dans un registre IANA.
Quelques mots sur la sécurité : dans certaines conditions, un observateur peut obtenir des informations sur l'état des tables (cf. l'attaque CRIME) même s'il ne peut déchiffrer les données protégées par TLS, celui-ci ne masquant pas la taille. Bien sûr, on pourrait remplir avec des données bidons mais cela annulerait l'avantage de la compression. La section 7 du RFC donne quelques idées sur des mécanismes de limitation du risque.
L'annexe A du RFC spécifie la table statique et ses 98
entrées. Elle a été composée à partir de l'analyse de trafic HTTP en
2018. L'ordre des entrées n'est pas arbitraire : vu comment sont
représentés les entiers, donc les index, dans QPACK, les entrées les
plus fréquentes sont en premier, car QPACK utilise moins de bits pour
les nombres les plus petits. Notez aussi que cette table comprend
les en-têtes HTTP « classiques », comme
content-length
ou
set-cookie
mais aussi ce que
HTTP/2 appelle les « pseudo-en-têtes », qui
commencent par deux-points. C'est par exemple
le cas de la méthode HTTP (GET
,
PUT
, etc), notée :method
ou, dans les réponses, du code de retour, noté
:status
(tiens, la table statique a une entrée
pour le code 403 mais pas pour le 404).
Si vous envisagez de programmer QPACK, l'annexe B contient des exemples de dialogue entre encodeur et décodeur, et l'annexe C du pseudo-code pour l'encodeur.
Date de publication du RFC : Juin 2022
Auteur(s) du RFC : R. Fielding (Adobe), M. Nottingham (Fastly), J. Reschke (greenbytes)
Chemin des normes
Réalisé dans le cadre du groupe de travail IETF httpbis
Première rédaction de cet article le 7 juin 2022
Que voilà une épaisse lecture (252 pages). Mais c'est parce qu'il s'agit de réécrire complètement la totalité des normes de HTTP. Pas le protocole lui-même, je vous rassure, HTTP ne change pas. Mais la rédaction de ses normes est profondément réorganisée, avec un RFC (notre RFC 9110) qui décrit une vision de haut niveau de HTTP, puis un autre RFC par version majeure de HTTP, décrivant les détails de syntaxe de chaque version.
Par exemple, HTTP/1 (RFC 9112) a un
encodage en texte alors que HTTP/2 (RFC 9113)
a un encodage binaire. Pourtant, tous les deux suivent les mêmes
principes, décrits dans ce RFC 9110 (méthodes comme
GET
, en-têtes de la requête et de la réponse,
codes de retour à trois chiffres…) mais avec des encodages
différents, chacun dans son propre RFC. Notre RFC 9110 est donc la vision de haut niveau de HTTP, commune à
toutes les versions, et d'autres RFC vous donneront les
détails. Inutile de dire que cette réorganisation a été un gros
travail, commencé en 2018.
Les trois versions de HTTP actuellement en large usage (1.1, 2 et 3) reposent toutes sur des concepts communs. Par exemple, les codes d'erreur (comme le fameux 404) sont les mêmes. Il n'est pas prévu, même à moyen terme, que les versions les plus anciennes soient abandonnées (HTTP/1.1 reste d'un usage très courant, et souvent pour de bonnes raisons). D'où cette réorganisations des normes HTTP, avec notre RFC 9110 qui décrit ce qui est commun aux trois versions, d'autres RFC communs aux trois versions, et un RFC par version :
Vous connaissez certainement déjà HTTP, mais notre RFC ne présuppose pas de connaissances préalables et explique tout en partant du début, ce que je fais donc également ici. Donc, HTTP est un protocole applicatif, client/serveur, sans état, qui permet l'accès et la modification de ressources distantes (une ressource pouvant être du texte, une image, et étant générée dynamiquement ou pas, le protocole est indépendant du format de la ressource ou de son mode de création, le RFC insiste bien sur ce point). Le client se connecte, envoie une requête, le serveur répond. HTTP ne fonctionne pas forcément de bout en bout, il peut y avoir des relais sur le trajet, et leur présence contribue beaucoup à certaines complexités de la norme.
S'il fallait résumer HTTP rapidement, on pourrait dire qu'il décrit un moyen d'interagir avec une ressource distante (la ressource peut être un fichier, un programme…). Il repose sur l'échange de messages, avec une requête du client vers le serveur et une réponse en sens inverse. Outre la méthode qui indique ce que le client veut faire avec la ressource, HTTP permet de transporter des métadonnées.
La section 3 du RFC décrit les concepts centraux de ce protocole,
comme celui de ressource présenté plus haut. (Qui est parfois appelé
« page » ou « fichier » mais ces termes ne sont pas assez
génériques. Une ressource n'est pas forcément une page HTML !) HTTP identifie
les ressources par des URI. Une représentation
est la forme concrète d'une ressource, les bits qu'on reçoit ou
envoie. (Du fait de la négociation de contenu et d'autres facteurs,
récupérer une ressource en utilisant le même URI ne donnera pas
forcément les mêmes bits, même s'ils sont censés être sémantiquement
équivalents.) La ressource n'est pas non plus forcément un fichier, pensez à une ressource qui indique l'heure qu'il est,
ou le temps qu'il fait, par exemple. Ou à l'URI
https://www.bortzmeyer.org/apps/random
qui vous
renvoie une page choisie aléatoirement de ce blog. HTTP agit
sur une ressource (dont le type n'est pas forcément connu) via une
méthode qui va peut-être retourner une représentation de cette
ressource. C'est ce qu'on nomme le principe REST et de
nombreuses API se réclament de ce principe.
HTTP est un protocole client/serveur. Le serveur attend le client. (Le client est parfois appelé user agent.) Entre les deux, HTTP utilisera un protocole de transport fiable, comme TCP (HTTP/1 et 2) ou QUIC (HTTP/3). Par défaut, HTTP est sans état : une fois une requête servie, le serveur oublie tout. Les clients sont très variés : il y a bien sûr les navigateurs Web, mais aussi les robots, des outils en ligne de commande comme wget, des objets connectés, des programmes vite faits en utilisant une des zillions de bibliothèques qui permettent de développer rapidement un client HTTP, des applications sur un ordiphone, etc. Notamment, il n'y a pas forcément un utilisateur humain derrière le client HTTP. (Pensez à cela si vous mettez des éléments d'interfaces qui demandent qu'un humain y réponde ; le client ne peut pas forcément faire de l'interactivité.)
Les messages envoyés par le client au serveur sont des requêtes et ceux envoyés par le serveur des réponses.
La section 2 du RFC explique ce qu'on attend d'un client ou d'un serveur HTTP conforme. Un point important et souvent ignoré est que HTTP ne donne pas de limites quantitatives à beaucoup de ses éléments. Par exemple, la longueur maximale de la première ligne de la requête (celle qui contient le chemin de la ressource) n'est pas spécifiée, car il serait trop difficile de définir une limite qui convienne à tous les cas, HTTP étant utilisé dans des contextes très différents. Comme les programmes ont forcément des limites, cela veut dire qu'on ne peut pas toujours compter sur une limite bien connue.
Une mise en œuvre conforme pour HTTP doit notamment bien gérer la notion de version de HTTP. Cette version s'exprime par deux chiffres séparés par un point, le premier chiffre étant la version majeure (1, 2 ou 3) et le second la mineure (il est optionnel, valant 0 par défaut, donc HTTP/2 veut dire la même chose que HTTP/2.0). Normalement, au sein d'une même version majeure, on doit pouvoir interopérer sans trop de problème alors qu'entre deux versions majeures, il peut y avoir incompatibilité totale. La sémantique est forcément la même (c'est du HTTP, après tout) mais la syntaxe peut être radicalement différente (pensez à l'encodage texte de HTTP/1 vs. le binaire de HTTP/2 et 3). Donc, être conforme à HTTP/1.1 veut dire lire ce RFC 9110 mais aussi le RFC 9112, qui décrit la syntaxe spécifique de HTTP/1.1.
Comme, dans la nature, des programmes ne sont pas corrects, le
RFC autorise du bout des lèvres à utiliser le contenu des champs
User-Agent:
ou Server:
de
l'en-tête pour s'ajuster à des bogues connues (mais, normalement, ce
doit être uniquement pour contourner des bogues, pas pour servir un
contenu différent).
De même qu'un client HTTP n'est pas forcément un navigateur Web, un serveur HTTP n'est pas forcément une grosse machine dans un centre de données chez un GAFA. Le serveur HTTP peut parfaitement être une imprimante, un petit objet connecté, une caméra de vidéosurveillance, un Raspberry Pi dans son coin… Le RFC parle de « serveur d'origine » pour le serveur qui va faire autorité pour les données servies. Pourquoi ce concept ? Parce que HTTP permet également l'insertion d'un certain nombre d'intermédiaires, les relais (proxy ou gateway en anglais), entre le client et le serveur d'origine. Leurs buts sont très variés. Par exemple, un relais (proxy, pour le RFC) dans le réseau local où se trouve le client HTTP peut servir à mémoriser les ressources Web les plus souvent demandées, pour améliorer les performances. Un relais (gateway ou reverse proxy, pour le RFC) qui est au contraire proche du serveur d'origine peut servir à répartir la charge entre diverses instances. Revenons à la mémorisation des ressources (caching en anglais). La mémoire (cache en anglais) est un stockage de ressources Web déjà visitées, prêtes à être envoyées aux clients locaux pour diminuer la latence. La mémorisation est un sujet suffisamment fréquent et important pour avoir son propre RFC, le RFC 9111.
On a vu que HTTP servait à agir sur des ressources distantes. Des
ressources, il y en a beaucoup. Comment les identifier ? Le Web va
utiliser des URI comme
identificateurs. Ces URI sont normalisés dans
le RFC 3986, mais qui ne spécifie qu'une
syntaxe générique. Chaque plan d'URI (la chaine
de caractères avant le deux-points, souvent
appelée à tort protocole) doit spécifier un certain nombre de
détails spécifique à ce plan. Pour les plans
http
et https
, cette
spécification est la section 4 de notre RFC. (Tous les plans sont
dans un
registre IANA.) Un URI de plan http
ou
https
indique forcément une autorité (un
identificateur du serveur d'origine, en pratique un nom de machine)
et un chemin (identificateur de la ressource à l'intérieur d'un
autorité. Ainsi, dans
https://www.afnic.fr/observatoire-ressources/consultations-publiques/
,
le plan est https
, l'autorité
www.afnic.fr
et le chemin
/observatoire-ressources/consultations-publiques/
. Le
port par défaut est 80 pour
http
et 443 pour
https
(tous les deux sont enregistrés
à l'IANA). La différence entre les deux plans est que
https
implique l'utilisation du protocole de
sécurité TLS
(RFC 8446), pour assurer notamment la
confidentialité des requêtes.
En théorie, un URI de plan http
et un autre
identique, sauf pour l'utilisation de https
,
sont complètement distincts. Ils ne représentent pas la même origine
(l'origine est un triplet {plan, machine, port}) et les deux
ressources peuvent être complètement différentes. Mais notre RFC
note que certaines normes violent ce principe, notamment celle sur
les cookies (RFC 6265),
avec parfois des conséquences fâcheuses pour la sécurité.
En HTTPS, puisque ce protocole s'appuie sur TLS, le serveur présente un certificat, que le client doit vérifier (section 4.3.4), en suivant les règles du RFC 6125.
Pour expliquer plusieurs des propriétés de HTTP, je vais beaucoup
utiliser le logiciel curl, un
client HTTP en ligne de commande, dont
l'option -v
permet d'afficher tout le dialogue
HTTP. Si vous voulez faire des essais vous aussi, interrompez
momentanément votre lecture pour installer curl. […] C'est fait ? On
peut reprendre ?
% curl -v http://www.hambers.mairie53.fr/ ... > GET / HTTP/1.1 > Host: www.hambers.mairie53.fr > User-Agent: curl/7.68.0 > Accept: */* > < HTTP/1.1 200 OK < Date: Wed, 02 Mar 2022 16:25:02 GMT < Server: Apache ... < Content-Length: 61516 < Content-Type: text/html; charset=UTF-8 < <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> ...
Nous avons vu que la requête et la réponse HTTP contenaient des métadonnées dans un en-tête composé de champs. (Il peut aussi y avoir un pied, une sorte de post-scriptum, mais c'est peu utilisé.) Chaque champ a un nom et une valeur. La section 5 du RFC détaille cet important concept. Les noms de champs sont insensibles à la casse. Ils sont enregistrés dans un registre IANA spécifique à HTTP (ils étaient avant dans le même registre que les champs du courrier électronique). Un client, un serveur ou un relais HTTP doivent ignorer les champs qu'ils ne connaissent pas, ce qui permet d'introduire de nouveaux champs sans tout casser. Le même champ peut apparaitre plusieurs fois. Comme pour d'autres éléments du protocole HTTP, la norme ne fixe pas de limite de taille. La valeur d'un champ peut donc être très grande.
La valeur d'un champ obéit à des règles qui dépendent du champ. Les caractères doivent être de l'ASCII, une limite très pénible de HTTP. Si on veut utiliser Unicode (ou un autre jeu de caractères), il faut l'encoder comme indiqué dans le RFC 8187. Le RFC rappelle qu'autrefois Latin-1 était autorisé (avec l'encodage du RFC 2047 pour les autres jeux) mais cela ne devrait normalement plus être le cas (mais ça se rencontre parfois encore). Si une valeur comprend plusieurs termes, ils doivent normalement être séparés par des virgules (et on met entre guillemets les valeurs qui comprennent des virgules). Les valeurs peuvent inclure des paramètres, écrits sous la forme nom=valeur. Certaines valeurs ont leur propre structure (RFC 8941). Ainsi, plusieurs champs peuvent inclure une estampille temporelle. La syntaxe pour celles-ci n'est hélas pas celle du RFC 3339 mais celle de l'IMF (RFC 5322), plus complexe et plus ambigüe. (Sans compter, vu l'âge de HTTP, qu'on rencontre parfois de vieux formats comme celui du RFC 850.) Voici des exemples de champs vus avec curl :
% curl -v http://confiance-numerique.clermont-universite.fr/ ... > GET / HTTP/1.1 > Host: confiance-numerique.clermont-universite.fr > User-Agent: curl/7.68.0 > Accept: */* < HTTP/1.1 200 OK < Date: Fri, 28 Jan 2022 17:21:38 GMT < Server: Apache/2.4.6 (CentOS) < Last-Modified: Tue, 01 Sep 2020 13:29:41 GMT < ETag: "1fbc1-5ae4082ec730d" < Accept-Ranges: bytes < Content-Length: 129985 < Content-Type: text/html; charset=UTF-8 < <!DOCTYPE HTML SYSTEM> <html> <head> <title>Séminaire Confiance Numérique</title>
Le client HTTP (curl) a envoyé trois champs,
Host:
(le serveur qu'on veut contacter),
User-Agent:
(une chaine de caractères décrivant
le client) et Accept:
(les formats acceptés,
ici tous). Le serveur a répondu avec divers champs comme
Server:
(l'équivalent du
User-Agent:
) et
Content-Type:
(le format utilisé, ici
HTML). Et voici ce qu'envoie le navigateur Firefox :
Host: localhost:8080 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive Upgrade-Insecure-Requests: 1 Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: none Sec-Fetch-User: ?1
On notera surtout un Accept:
plus complexe
(curl accepte tout car il ne s'occupe pas de l'affichage).
Maintenant, les messages (requêtes et réponses). La façon exacte dont ils sont transmis dépend de la version de HTTP. Par exemple, la version 1 les encode en texte alors que les versions 2 et 3 préfèrent le binaire. Autre exemple, la version 3 ne prévoit pas de mécanisme de début et de fin d'un message car chaque ruisseau QUIC ne porte qu'un seul message, un peu comme les versions 0 de HTTP, avec le ruisseau QUIC au lieu de la connexion TCP (notez qu'avec TCP sans TLS, le client peut ne pas savoir s'il a bien reçu toutes les données). La section 6 de notre RFC ne donne donc qu'une description abstraite. Un message comprend donc une information de contrôle (la première ligne, dans le cas de HTTP/1, un « pseudo en-tête » avec des noms de champs commençant par un deux-points pour les autres versions), un en-tête, un corps (optionnel) et un pied (également optionnel). L'information de contrôle donne plusieurs informations nécessaires pour la suite, comme la version de HTTP utilisée. Le contenu (le corps) est juste une suite d'octets, que HTTP transporte sans l'interpréter (ce n'est pas forcément de l'HTML). Dans la réponse, l'information de contrôle comprend notamment un code numérique de trois chiffres, qui indique comment la requête a été traitée (ou pas).
Beaucoup moins connu que l'en-tête, un message peut aussi comporter un pied, également composé de champs « nom: valeur ». Il est nécessaire de les utiliser dans les cas où l'information est générée dynamiquement et que certaines choses ne peuvent être déterminées qu'après coup (une signature numérique, par exemple).
Dans le cas le plus simple, le client HTTP parle directement au
serveur d'origine et il n'y a pas de complications de routage du
message. Le serveur traite le message reçu, point. Mais HTTP permet
d'autres cas, par exemple avec un relais qui
reçoit le message avant de le transmettre au « vrai » serveur
(section 7 du RFC). Ainsi, dans une requête, l'information de
contrôle n'est pas forcément un simple chemin
(/publications/cahiers-soutenabilites
) mais
peut être un URL complet
(https://www.strategie.gouv.fr/publications/cahiers-soutenabilites
). C'est
ce que fait le client HTTP s'il est configuré pour utiliser un
relais, par exemple pour mémoriser les réponses des requêtes (RFC 9111), ou bien parce que l'accès direct aux
ports 80 et 443 est
bloqué et qu'on est obligé d'utiliser un relais. Dans le cas où la
ressource demandées est identifiée par un URL complet, le relais
doit alors se transformer en client HTTP et faire une requête vers
le serveur d'origine (ou bien vers un autre relais…).
La section 8 de notre RFC s'attaque à une notion cruciale en
HTTP, celle de représentation. La
représentation d'une ressource est la suite d'octets qu'on obtient
en réponse à une requête HTTP (« représentation » est donc plus
concret que « ressource »). Une même ressource peut avoir plusieurs
représentations, par exemple selon les métadonnées que le client a
indiqué dans sa requête. Le type de la représentation est indiqué par le champ
Content-Type:
de l'en-tête (et aussi par
Content-Encoding:
). Sa valeur est un
type MIME (RFC 2046). Voici par exemple le type de la page que vous êtes
en train de lire :
Content-Type: text/html; charset=UTF-8
(Notez que le paramètre charset
est mal nommé,
c'est en fait un encodage, pas un
jeu de caractères. L'erreur vient du fait que
dans les vieilles normes comme ISO-8859-1, les
deux concepts étaient confondus.)
Normalement, du fait de ce Content-Type:
, le
client HTTP n'a pas à deviner le type de la représentation, il se
fie à ce que le serveur raconte. Ceci dit, certains clients ont la
mauvaise idée de chercher à deviner le
type. Cette divination est toujours incertaine (plusieurs
types de données peuvent se ressembler) et ouvre même la possibilité
de failles de sécurité.
Un autre champ, Content-Language:
, indique
la langue de la représentation récupérée. Sa
valeur est une étiquette de langue, au sens
du RFC 5646. Si le texte est multilingue, ce
champ peut prendre plusieurs valeurs. Le RFC illustre cela avec le
traité de Waitangi, qui est en
maori et en anglais :
Content-Language: mi, en
Attention, la seule présence de différentes langues ne signifie pas qu'il faut mettre plusieurs étiquettes de langue. Un cours d'introduction à l'arabe écrit en français, pour un public francophone, sera :
Content-Language: fr
Les étiquettes de langue peuvent être plus complexes que l'indication de la seule langue, mais il me semble que c'est rarement utilisé sur le Web.
La taille de la représentation, elle, est exprimée avec
Content-Length:
, un champ très pratique pour le
client HTTP qui sait ainsi combien d'octets il va devoir lire (avant
HTTP/1, c'était facile, on lisait jusqu'à la fin de la connexion
TCP ;
mais ça ne marche plus depuis qu'il y a des connexions persistentes
et, de toute façon, en l'absence de TLS, cela ne permettait pas de
détecter des coupures prématurées). Évidemment, le client doit
rester paranoïaque et supposer que l'information puisse être
fausse. curl (avec -v
) avertit ainsi, si la
taille indiquée est trop faible :
* Excess found in a read: excess = 1, size = 12, maxdownload = 12, bytecount = 0
Si la taille indiquée est trop grande, curl attend pour essayer de lire davantage sur le connexion qui reste ouverte. Autre raison d'être paranoïaque, la taille indiquée peut être énorme, menant par exemple un client imprudent, qui allouerait la mémoire demandée à épuiser celle-ci. Sans compter l'éventualité d'un débordement d'entier si la taille ne peut pas être représentée dans les entiers utilisés par le client HTTP.
Ensuite vient un autre point pas forcément très connu : les
validateurs. HTTP permet d'indiquer des pré-conditions à la
récupération d'une ressource, pour épargner le réseau. Un client
HTTP peut ainsi demander « donne-moi cette ressource, si elle n'a
pas changé ». Pour cela, HTTP repose sur ces validateurs, qui sont
des métadonnées qui accompagnent la requête (avec des champs qui
expriment la requête conditionnelle, comme
If-Modified-Since:
, et qui sont détaillés en
section 13) et que le serveur vérifiera. Il existe deux sortes de
validateurs, les forts et les faibles. Les faibles sont faciles à
générer mais ne garantissent pas une comparaison réussie, les forts
sont plus difficiles à faire mais sont plus fiables. Par exemple, un
condensat du contenu est fort. Il changera
forcément (sauf malchance inouïe) dès qu'on changera un seul bit du
contenu. Si le contenu est géré par un VCS, celui-ci fournit également des
validateurs forts : l'identificateur de
commit. Au contraire, une
estampille temporelle est un validateur faible. Si sa résolution est
d'une seconde, deux modifications dans la même seconde ne seront pas
détectées et le serveur croira à tort que le contenu n'a pas
changé.
Pour connaitre la valeur actuelle d'un futur validateur, le
client HTTP dispose de champs comme
Last-Modified:
(une estampille temporelle) ou
ETag:
(Entity Tag,
l'étiquette de la ressource, une valeur opaque, qui peut s'utiliser
avec des requêtes conditionnelles comme
If-None-Match:
). Voici un exemple :
Last-Modified: Mon, 07 Feb 2022 12:20:20 GMT ETag: "5278-5d76c9fc1c9f4"
(Le serveur utilisé était un Apache. Par
défaut, Apache génère des étiquettes qui sont un
condensat de divers attributs du fichier
comme l'inœud, la taille et la date de
modification. Apache permet de configurer
cet algorithme. Rappelez-vous que l'étiquette est opaque, le
serveur peut donc la générer comme il veut, il doit juste s'assurer
qu'elle change à chaque modification de la ressource. Le serveur peut par
exemple utiliser un SHA-1 du contenu de la ressource.)
A priori, l'étiquette de la ressource est un validateur fort,
autrement, le serveur doit la préfixer par W/
(W pour Weak).
Passons maintenant aux méthodes (section 9
du RFC). Il y a très longtemps, HTTP n'avait qu'une seule méthode
pour agir sur les ressources, la méthode
GET
. Désormais, il y a nettement plus de
méthodes, chacune agissant sur la ressource indiquée d'une manière
différente et ayant donc une sémantique différente. Par exemple,
GET
va récupérer une représentation de la
ressource, alors que PUT
va au contraire écrire
le contenu envoyé, remplaçant celui de la ressource et que
DELETE
va… détruire la ressource. La liste
complète des méthodes figure dans un
registre IANA.
Certaines des méthodes sont dites sûres car elles ne modifient
pas la ressource et ne casseront donc rien. Bien sûr, une méthode
sûre peut avoir des effets de bord (comme d'écrire une ligne dans le
journal du serveur, mais ce n'est pas la
faute du client). GET
, HEAD
et les moins connues OPTIONS
et
TRACE
sont sûres. Du fait de cette garantie de
sûreté, un programme qui ne fait que des requêtes sûres a moins
d'inquiétudes à avoir, notamment s'il agit sur la base
d'informations qu'il ne contrôle pas. Ainsi, le
ramasseur d'un moteur de
recherche ne fait a priori que des requêtes sûres, pour
éviter qu'une page Web malveillante ne l'entraine à effectuer des
opérations qui peuvent changer le contenu des sites Web visités.
Une autre propriété importante d'une méthode est d'être
idempotente ou pas. Une méthode idempotente a
le même effet qu'on l'exécute une ou N fois. Les méthodes sûres sont
toutes idempotentes mais l'inverse n'est pas vrai :
PUT
et DELETE
sont
idempotentes (qu'on détruise une ressource une ou N fois donnera le
même résultat : la ressource est supprimée) mais pas
sûres. L'intérêt de cette propriété d'idempotence est qu'elles
peuvent être répétées sans risque, par exemple si le réseau a eu un
problème et qu'on n'est pas certain que la requête ait été
exécutée. Les méthodes non-idempotentes ne doivent pas, par contre,
être répétées aveuglément.
La méthode la plus connue et sans doute la plus utilisée,
GET
, permet de récupérer une représentation
d'une ressource. La syntaxe avec laquelle s'exprime le chemin de
cette ressource fait penser à l'arborescence d'un système
de fichiers et c'est en effet souvent ainsi que c'est
mis en œuvre dans les serveurs (par exemple dans Apache, où le chemin, mettons
/foo/bar
, est ajouté à la fin de la variable de
configuration DocumentRoot
, avant d'être
récupéré sur le système de fichiers : si
DocumentRoot
vaut
/var/www
, le fichier demandé sera
/var/www/foo/bar
). Mais ce n'est pas une
obligation de HTTP, qui ne normalise que le protocole entre le client
et le serveur, pas la façon dont le serveur obtient les
ressources.
La méthode HEAD
fait la même chose que
GET
mais sans renvoyer la représentation de la
ressource.
POST
est plus compliquée. Contrairement à
GET
, la requête contient des données qui vont
être envoyés au serveur. Celui-ci va les
traiter. POST
est souvent utilisé pour
soumettre le contenu d'un formulaire Web, par
exemple pour envoyer un texte qui sera le contenu d'un commentaire
lors d'une discussion sur un forum Web. Avec
GET
, POST
est probablement
la méthode la plus souvent vue sur le Web.
PUT
, lui, est également accompagné de
données qui vont être écrites à la place de la ressource
désignée. On peut donc mettre en œuvre un serveur de fichiers
distant avec des PUT
et des
GET
. On peut y ajouter
DELETE
pour supprimer les ressources devenues
inutiles.
La méthode CONNECT
est plus complexe. Elle
n'agit pas sur une ressource mais permet d'établir une connexion
avec un service distant. Sa principale utilité est de permettre
d'établir un tunnel au-dessus de
HTTP. Ainsi :
CONNECT server.example.com:80 HTTP/1.1 Host: server.example.com
va établir une connexion avec
server.example.com
et les octets envoyés par la
suite sur cette connexion HTTP seront relayés aveuglément vers
server.example.com
.
Quant à la méthode OPTIONS
, elle permet
d'obtenir des informations sur les options gérées par le
serveur. curl permet d'indiquer une méthode avec son option
--request
(ou -X
) :
% curl -v --request OPTIONS https://www.bortzmeyer.org/ ... > OPTIONS / HTTP/2 > Host: www.bortzmeyer.org > user-agent: curl/7.68.0 > accept: */* > ... < HTTP/2 200 < permissions-policy: interest-cohort=() < allow: POST,OPTIONS,HEAD,GET ...
La section 10 du RFC est ensuite une longue section qui décrit le contexte des messages HTTP, c'est-à-dire les métadonnées qui accompagnent requêtes et réponses. Je ne vais évidemment pas en reprendre toute la liste ici. Juste quelques exemples de champs intéressants :
From:
permet d'indiquer l'adresse de
courrier du
responsable du logiciel. Il est surtout utilisé par les
bots (par exemple ceux qui ramassent les pages pour le
compte d'un moteur de recherche) pour
indiquer qui contacter si le bot se comporte mal, par exemple en
faisant trop de requêtes. Comme le rappelle le RFC, un navigateur
ordinaire ne doit évidemment pas transmettre une telle donnée
personnelle à tous les sites Web visités !Referer:
(oui, avec une faute d'orthographe) sert à indiquer
l'URL d'où vient le client HTTP. Le Web étant fondé sur l'idée
d'hypertexte, l'utilisateur est peut-être
venu ici en suivant un lien, et il peut ainsi indiquer où il a
trouvé ce lien, ce qui peut permettre au webmestre de voir d'où
viennent ses visiteurs. Lui aussi pose des problèmes de vie privée, et j'ai toujours été
surpris que le Tor Browser l'envoie.User-Agent:
indique le type du client
HTTP. À part s'amuser en regardant le genre de visiteurs qu'on a,
il n'a pas de vraie utilité, le Web reposant sur des normes, et
précisant une structure et pas une présentation, il ne
devrait pas y avoir besoin de changer une ressource en fonction du
logiciel du visiteur. Mais c'est quand même ce que font certains
serveurs HTTP, poussant les clients à mentir pour obtenir un
certain résultat, ce qui donne des champs
User-Agent:
ridicules comme (vu sur ce blog)
Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75
Safari/537.36
(probablement le navigateur
Safari indiquant autant de logiciels que possible ; le RFC dit qu'il ne faut pas le
faire mais c'est courant, pour tenir compte de serveurs qui
interprètent ce champ). Là encore, on a une métadonnée
qui contribue puissamment à la fuite d'information si commune sur
le Web (votre client HTTP est certainement trop bavard). Le
User-Agent:
est très utile pour le
fingerprinting, l'identification d'un visiteur
particulier, comme le démontre le Panopticlick.Server:
est l'équivalent de
User-Agent:
mais pour le serveur.Jusqu'à présent, on a supposé que les ressources servies étaient
accessibles à tous et toutes. Mais en pratique, on souhaite parfois
servir du contenu à accès restreint et on veut donc n'autoriser que certains visiteurs. Il
faut donc disposer de mécanismes
d'authentification, exposés dans la section
11 du RFC. HTTP n'a pas un mécanisme unique
d'authentification. Chaque mécanisme est identifié par un nom (et
les possibilités sont dans un
registre IANA). Le serveur indique le mécanisme à utiliser
dans un champ WWW-Authenticate:
de sa première
réponse. Par exemple, basic
, normalisé dans le
RFC 7617, est un mécanisme simple de
mot de passe, alors que
digest
(normalisé dans le RFC 7616) permet de s'authentifier via un
défi/réponse. Le mécanisme est spécifique à
un royaume, une information donnée par le serveur pour le cas où le
même serveur gérerait des types d'authentification différents selon
la ressource.
Voici un exemple d'authentification (avec le service
d'administration d'un serveur
dnsdist, celui utilisé pour mon résolveur
public) où l'identificateur est admin
et
le mot de passe 2e12
:
% curl -v --user admin:2e12 https://doh.bortzmeyer.fr:8080/ > GET / HTTP/1.1 > Host: doh.bortzmeyer.fr:8080 > Authorization: Basic YWRtbW...OTcyM= > User-Agent: curl/7.68.0 > Accept: */* > ... < HTTP/1.1 200 OK ...
La représentation renvoyée peut dépendre du client, c'est ce
qu'on nomme la négociation de contenu (section 12 du RFC). La
méthode officielle est que le client annonce avec le champ
Accept:
les types
de données qu'il accepte, et le serveur lui envoie de
préférence ce qu'il a demandé. (C'est utilisé sur ce blog pour les
images. En pratique, ça ne se passe pas toujours bien.)
La demande du client n'est pas strictement binaire « je veux du
format WebP ». Elle peut s'exprimer de
manière plus nuancée, via le système de qualité. Ainsi :
Accept: text/plain; q=0.5, text/html
signifie que le client comprend le texte brut et l'HTML mais préfère ce dernier (le poids par défaut est 1, supérieur, donc, au 0,5 du texte brut).
La négociation de contenu ne s'applique pas qu'au format des
représentations, elle peut aussi s'appliquer à la
langue, avec le champ
Accept-Language:
. Ainsi, en disant :
Accept-Language: da, en;q=0.8
veut dire « je préfère le danois (poids de 1 par défaut), mais j'accepte l'anglais ». En pratique, ce n'est pas très utile sur le Web car cela ne permet pas d'indiquer la qualité de la traduction. Si on indique qu'on préfère le français, mais qu'on peut lire l'anglais, en visitant des sites Web d'organisations internationales, on se retrouve avec un texte français mal traduit, alors qu'on aurait préféré la version originale. En outre, comme beaucoup de champs de l'en-tête de la requête, il contribue à identifier le client (fingerprinting). C'est d'autant plus gênant que l'indication des langues préférées peut vous signaler à l'attention de gens peu sympathiques, si ces langues sont celles d'une minorité opprimée.
Comme la représentation envoyée peut dépendre de ces demandes du
client, le serveur doit indiquer dans sa réponse s'il a effectivement
tenu compte de la négociation de contenu. C'est notamment important
pour les relais Web qui mémorisent le contenu des réponses (RFC 9111). Le champ Vary:
permet d'indiquer de quoi a réellement dépendu la réponse. Ainsi :
Vary: accept-language
indique que la réponse ne dépendait que de la langue. Si un client d'un relais Web demande la même ressource mais avec une autre langue, il ne faut pas lui donner le contenu mémorisé.
HTTP permet d'exprimer des requêtes conditionnelles. Un client HTTP peut par exemple demander une ressource « sauf si elle n'a pas été modifiée depuis 08:00 ». Cela permet d'économiser des ressources, notamment dans les relais qui mémorisent RFC 9111. C'est également utile aux clients de syndication, qui récupèrent régulièrement (polling) le flux Atom pour voir s'il a changé. Les requêtes conditionnelles permettent, la plupart du temps, d'éviter tout téléchargement de ce flux.
Un autre scénario d'utilisation des requêtes conditionnelles est
le cas de la mise à jour perdue (lost
update). Prenons un client qui met à jour une ressource,
en récupérant d'abord son état actuel (avec un
GET
), en la modifiant, puis en téléversant la
version modifiée (avec par exemple un PUT
). Si
deux clients font l'opération à peu près en même temps, il y a un
risque d'une séquence :
GET
,PUT
avec sa version
modifiée,
Les mises à jour conditionnelles résoudraient ce problème : si
Client 2 fait sa mise à jour en ajoutant « seulement si la ressource
n'a pas changé », son PUT
sera refusé, il devra
refaire un GET
et il récupérera alors les
changeements de Client 1 avant d'appliquer les siens.
En pratique, les requêtes conditionnelles se font avec des champs
comme If-Modified-Since:
dont la valeur est une
date. Par exemple, le client de syndication qui a récupéré une page
le 24 février à 18:11, va envoyer un If-Modified-Since:
Thu, 24 Feb 2022 18:11:00 GMT
et le serveur ne lui
enverra la ressource Atom ou
RSS que si elle est plus récente (il recevra
un code de retour 304 dans le cas contraire). Autre exemple de
champ d'en-tête pour les requêtes conditionnelles,
If-Match:
. Ce champ demande que la requête ne
soit exécutée que si la ressource correspond à la valeur du
If-Match:
. La valeur est un validateur, comme
expliqué plus haut. If-Match:
permet ainsi de
résoudre le problème de la mise à jour perdue.
Les ressources chargées en HTTP peuvent être de grande
taille. Parfois, le réseau a un hoquet et le transfert s'arrête. Il
serait agréable de pouvoir ensuite reprendre là où on s'était
arrêté, au lieu de tout reprendre à zéro. HTTP le permet via les
requêtes d'un intervalle (section 14). Le client indique par le
champ Range:
quel intervalle de la ressource il
souhaite. Cet intervalle peut se formuler en plusieurs unités, le
champ Accept-Ranges:
permettant au serveur
d'indiquer qu'il gère ces demandes, et dans quelles unités. En
pratique, seuls les octets
marchent réellement. Par exemple, ici, j'utilise curl pour récupérer
50 octets d'un article :
% curl -v --range 3100-3150 https://www.bortzmeyer.org/1.html ... > GET /1.html HTTP/2 > Host: www.bortzmeyer.org > range: bytes=3100-3150 ... ocuments, qui forme l'ossature de la <b><a class="
Passons maintenant aux codes de retour HTTP (section 15). Il y en a au moins un qui est célèbre, 404, qui indique que la ressource demandée n'a pas été trouvée sur ce serveur et qui est souvent directement visible par l'utilisateur humain (pensez aux « pages 404 » d'erreur). Ces codes sont composés de trois chiffres, le premier indiquant la classe :
Les deux autres chiffres fournissent des détails mais un client HTTP simple peut se contenter de comprendre la classe et d'ignorer les deux autres chiffres. Par exemple, 100 signifie que le serveur a compris la requête mais qu'il faut encore attendre pour la vraie réponse, 200 veut dire qu'il n'y a rien à dire, que tout s'est bien passé comme demandé, 308 indique qu'il faut aller voir à un autre URL, 404, comme signalé plus haut, indique que le serveur n'a pas trouvé la ressource, 500 est une erreur générique du serveur, en général renvoyée quand le serveur a eu un problème imprévu. Peut-être connaissez-vous également :
PUT
),De nouveaux codes sont créés de temps en temps, et mis dans le registre IANA.
curl -v
vous affichera entre autres ce code
de retour. Si vous ne voulez que le code, et pas tous les messages
que l'utilisation de -v
entrainera, l'option
--write-out
est bien pratique :
% curl --silent --write-out "%{http_code}\n" --output /dev/null https://www.bortzmeyer.org/1.html 200 % curl --silent --write-out "%{http_code}\n" --output /dev/null https://www.bortzmeyer.org/2.html 404
Sinon, pour rire avec les codes de statut HTTP, il existe des photos de chats et une proposition d'illustrer ces codes par des émojis.
Beaucoup de choses dans HTTP peuvent être étendues (section 16). On peut créer de nouvelles méthodes, de nouveaux codes de retour, etc. Un logiciel client ou serveur ne doit donc pas s'étonner de voir apparaitre des questions ou des réponses qui n'existaient pas quand il a été programmé.
Ainsi, des nouvelles méthodes, en sus des traditionnelles
GET
, POST
,
PUT
, etc , peuvent être créées, comme l'avait
été PATCH
par le RFC 5789. La liste à jour des méthodes est dans un
registre IANA. Si vous programmez côté serveur, et que vous utilisez l'interface
CGI, la méthode est indiquée dans la variable
REQUEST_METHOD
et vous pouvez la tester, par
exemple ici en Python :
if environ["REQUEST_METHOD"] == "FOOBAR": ... Do something useful else: return unsupported(start_response, environ["REQUEST_METHOD"])
Des codes de retour peuvent également être ajoutés, comme le 451 (censure) du RFC 7725. Là aussi, la liste faisant autorité est le registre IANA.
Bien sûr, le registre le plus dynamique, celui qui voit le plus
d'ajouts, est celui des champs de
l'en-tête. Mais attention : du fait qu'il bouge beaucoup, les
nouveaux champs ne seront pas compris et utilisés par une bonne
partie des logiciels. (Au passage, notre RFC rappelle que l'ancienne
convention de préfixer les noms de champs non officiels par un
X-
a été abandonnée, par le RFC 6648.)
Et enfin, on peut étendre les listes de mécanismes d'authentification, et plusieurs autres.
Très utilisé, HTTP a évidemment connu sa part de problèmes de sécurité. La section 17 du RFC analyse les principaux risques. (Certains risques spécifiques sont traités dans d'autres RFC. Ainsi, les problèmes posés par l'analyse de l'encodage textuel de HTTP/1 sont étudiés dans le RFC 9112. Ceux liés aux URL sont dans le RFC 3986.) D'autre part, beaucoup de problème de sécurité du Web viennent :
Cette section 17 se concentre sur les problèmes de sécurité de HTTP, ce qui est déjà pas mal. Le RFC recommande la lecture des documents OWASP pour le reste.
Bon, premier problème, la notion d'autorité. Une réponse fait
autorité si elle vient de l'origine, telle
qu'indiquée dans l'URL. Le client HTTP va donc dépendre du mécanisme
de résolution de nom. Si, par exemple, la machine du client utilise
un résolveur DNS menteur, tout est
fichu. On croit aller sur http://pornhub.com/
et on se retrouve sur une page Web de
l'ARCOM. Il est donc crucial que cette
résolution de noms soit sécurisée, par exemple en utilisant un
résolveur DNS de confiance, et qui valide les réponses avec
DNSSEC. HTTPS protège partiellement. Une des
raisons pour lesquelles sa protection n'est pas parfaite est qu'il
est compliqué de valider proprement (cf. RFC 7525). Et puis les problèmes sont souvent non techniques,
par exemple la plupart des tentatives
d'hameçonnage ne vont pas viser l'autorité
mais la perception que l'utilisateur en a. Une page Web copiée sur
celle d'une banque peut être prise pour celle de la banque même si,
techniquement, il n'y a eu aucune subversion des techniques de
sécurité. Le RFC recommande qu'au minimum, les navigateurs Web
permettent d'examiner facilement l'URL vers lequel va un lien, et de
l'analyser (beaucoup d'utilisateurs vont croire, en voyant
https://nimportequoi.example/banque-de-confiance.com
que le nom de domaine est
banque-de-confiance.com
…).
On a vu qu'HTTP n'est pas forcément de bout en bout et qu'il est même fréquent que des intermédiaires se trouvent sur le trajet. Évidemment, un tel intermédaire est idéalement placé pour certaines attaques. Bref, il ne faut utiliser que des intermédiaires de confiance et bien gérés. (De nombreuses organisations placent sur le trajet de leurs requêtes HTTP des boites noires au logiciel privateur qui espionnent le trafic et font Dieu sait quoi avec les données récoltées.)
HTTP est juste un protocole entre le client et le serveur. Le
client demande une ressource, le serveur lui envoie. D'où le serveur
a-t-il tiré cette ressource ? Ce n'est pas l'affaire de HTTP. En
pratique, il est fréquent que le serveur ait simplement lu un
fichier pré-existant sur ses disques et, en outre, que le chemin
menant à ce fichier vienne d'une simple transformation de l'URL. Par
exemple, Apache,
avec la directive DocumentRoot
valant
/var/doc/mon-beau-site
et une requête HTTP
GET /toto/tata.html
va chercher un fichier
/var/www/mon-beau-site/toto/tata.html
. Dans ce
cas, attention, certaines manipulations sur le chemin donné en
paramètre à GET
peuvent donner au client
davantage d'accès que ce qui était voulu. Ainsi, sans précautions
particulières, une requête GET
/../toto/tata.html
serait traduite en
/var/www/mon-beau-site/../toto/tata.html
, ce
qui, sur Unix, équivaudra à
/var/www/toto/tata.html
, où il n'était
peut-être pas prévu que le client puisse se promener. Les auteurs de
serveurs doivent donc être vigilants : ce qui vient du client n'est
pas digne de confiance.
Autre risque lorsqu'on fait une confiance aveugle aux données envoyées par le client, l'injection. Ces données, par exemple le chemin dans l'URL, sont traitées par des langages qui ont des règles spéciales pour certains caractères. Si un de ces caractères se retrouve dans l'URL, et qe le programme, côté serveur, n'est pas prudent avec les données extérieures, le ou les caractères spéciaux seront interprétés, avec parfois d'intéressantes failles de sécurité à la clé. (Mais, attention, tester la présence de « caractères dangereux » n'est en général pas une bonne idée.)
La liste des questions de sécurité liées à HTTP ne s'arrête pas là. On a vu que HTTP ne mettait pas de limite de taille à des éléments comme l'URL. Un analyseur imprudent, côté serveur, peut se faire attaquer par un client qui enverrait un chemin d'URL très long, déclenchant par exemple un débordement de tableau.
HTTP est un protocole très bavard, et un client HTTP possède
beaucoup d'informations sur l'utilisateur humain qui est
derrière. Le client doit donc faire très attention à ne pas envoyer
ces données. Le RFC ne donne pas d'exemple précis mais on peut par
exemple penser au champ Referer
qui indique
l'URL d'où on vient. Si le client l'envoie systématiquement, et que
l'utilisateur visitait un site Web interne de l'organisation avant de
cliquer vers un lien externe, son navigateur enverra des détails sur
le site Web interne. Autre cas important,
un champ comme Accept-Language
, qu'on peut
estimer utile dans certains cas, est dangereux pour la vie privée,
transmettant une information qui peut être sensible, par exemple si
on a indiqué une langue minoritaire et mal vue dans son pays. Et
User-Agent
facilite le ciblage d'éventuelles
attaques du serveur contre le client.
Du fait de ce caractère bavard, et aussi parce que, sur l'Internet, il y a des choses qu'on ne peut pas dissimuler facilement (comme l'adresse IP source), ce que le serveur stocke dans ses journaux est donc sensible du point de vue de la vie privée. Des lois comme la loi Informatique & Libertés encadrent la gestion de telles bases de données personnelles. Le contenu de ces journaux doit donc être protégé contre les accès illégitimes.
Comme HTTP est bavard et que le client envoie beaucoup de choses
(comme les Accept-Language
et
User-Agent
cités plus haut), le serveur peut
relativement facilement faire du
fingerprinting,
c'est-à-dire reconnaitre un client HTTP parmi des dizaines ou des
centaines de milliers d'aures. (Vous ne me croyez pas ? Regardez le
Panopticlick.) Un serveur peut ainsi suivre un client
à la trace, même sans
cookies (voir
« A
Survey on Web Tracking: Mechanisms, Implications, and
Defenses »).
Voilà, et encore je n'ai présenté ici qu'une partie des questions de sécurité liées à l'utilisation de HTTP. Lisez le RFC pour en savoir plus. Passons maintenant aux différents registres IANA qui servent à stocker les différents éléments du protocole HTTP. Je les ai présenté (au moins une partie d'entre eux !) plus haut mais je n'ai pas parlé de la politique d'enregistrement de nouvaux éléments. En suivant la terminologie du RFC 8126, il y a entre autres le registre des méthodes (pour ajouter une nouvelle méthode, il faut suivre la politique « Examen par l'IETF », une des plus lourdes), le registre des codes de retour (même politique), le registre des champs (désormais séparé de celui des champs du courrier, politique « Spécification nécessaire »), etc.
Ah, et si vous voulez la syntaxe complète de HTTP sous forme d'une grammaire formelle, lisez l'ABNF en annexe A.
Et avec un langage de programmation ? Vu le succès de HTTP et sa présence partout, il n'est pas étonnant que tous les langages de programmation permettent facilement de faire des requêtes HTTP. HTTP, en tout cas HTTP/1, est suffisamment simple pour qu'on puisse le programmer soi-même en appelant les fonctions réseau de bas niveau, mais pourquoi s'embêter ? Utilisons les bibliothèques prévues à cet effet et commençons par le langage Python. D'abord avec la bibliothèque standard http.client :
conn = http.client.HTTPConnection(HOST) conn.request("GET", PATH) result = conn.getresponse() body = result.read().decode()
Et hop, la variable body
contient une
représentation de la ressource demandée (le programme complet est en
sample-http-client.py
). En pratique, la plupart des
programmeurs Python utiliseront sans doute une autre
bibliothèque standard, qui n'est pas spécifique à HTTP et
permet de traiter des URL quelconques (cela
donne le programme sample-http-urllib.py
). D'encore
plus haut niveau (mais pas incluse dans la bibliothèque standard, ce
qui ajoute une dépendance à votre programme) est la bibliothèque
Requests, souvent utilisée (voir par exemple sample-http-requests.py
).
Ensuite, avec le langage Go. Là aussi, il dispose de HTTP dans sa bibliothèque standard :
response, err := http.Get(Url) defer response.Body.Close() body, err := ioutil.ReadAll(response.Body)
Le programme complet est sample-http.go
.
Et ici un client en Elixir, utilisant la bibliothèque HTTPoison :
HTTPoison.start() {:ok, result} = HTTPoison.get(@url)
La version longue est en
.sample-http.ex
L'annexe B de notre RFC fait la liste des principaux changements depuis les précédents RFC. Je l'ai dit, le protocole ne change pas réellement mais il y a quand même quelques modifications, notamment des clarifications de textes trop ambigus (par exemple la définition des intervalles). Et bien sûr le gros changement est qu'il y a désormais une définition abstraite de ce qu'est un message HTTP, séparée des définitions concrètes pour les trois versions de HTTP en service. En outre, il y a désormais des recommandations explicites de taille minimale à accepter pour certains élements (par exemple 8 000 octets pour les URI).
HTTP est, comme vous le savez, un immense succès, dû à la place prise par le Web, dont il est le protocole de référence. Le RFC résume l'histoire de HTTP :
GET
, et qui n'a été
documenté qu'après,Date de publication du RFC : Juin 2022
Auteur(s) du RFC : R. Fielding (Adobe), M. Nottingham (Fastly), J. Reschke (greenbytes)
Chemin des normes
Réalisé dans le cadre du groupe de travail IETF httpbis
Première rédaction de cet article le 7 juin 2022
Ce nouveau RFC normalise HTTP/1.1, la plus ancienne version de HTTP encore en service. Il décrit les détails de comment les messages sont représentés sur le réseau, la sémantique de haut niveau étant désormais dans un document séparé, le RFC 9110. Ensemble, ces deux RFC remplacent le RFC 7230.
HTTP est certainement le protocole Internet le plus connu. Il en existe plusieurs versions, ayant toutes en commun la sémantique normalisée dans le RFC 9110. Les versions les plus récentes, HTTP/2 et HTTP/3 sont loin d'avoir remplacé la version 1, plus précisément 1.1, objet de notre RFC et toujours largement répandue. Un serveur HTTP actuel doit donc gérer au moins cette version. (Par exemple, en octobre 2021, les ramasseurs de Google et Baidu utilisaient toujours exclusivement HTTP/1.1.)
Un des avantages de HTTP/1, et qui explique sa longévité, est que c'est un protocole simple, fondé sur du texte et qu'il est donc relativement facile d'écrire clients et serveurs. D'ailleurs, pour illustrer cet article, je vais prendre exemple sur un simple serveur HTTP/1 que j'ai écrit (le code source complet est disponible ici). Le serveur ne gère que HTTP/1 (les autres versions sont plus complexes) et ne vise pas l'utilisation en production : c'est une simple démonstration. Il est écrit en Elixir. Bien sûr, Elixir, comme tous les langages de programmation sérieux, dispose de bibliothèques pour créer des serveurs HTTP (notamment Cowboy). Le programme que j'ai écrit ne vise pas à les concurrencer : si on veut un serveur HTTP pour Elixir, Cowboy est un bien meilleur choix ! C'est en référence à Cowboy que mon modeste serveur se nomme Indian.
Commençons par le commencement, la section 1 de notre RFC rappelle les bases de HTTP (décrites plus en détail dans le RFC 9110).
La section 2 attaque ce qui est spécifique à la version 1 de
HTTP. Avec les URL de plan http:
, on
commence par établir une connexion TCP avec le serveur. Ensuite, un
message en HTTP/1 commence par une ligne de démarrage, suivie d'un
CRLF (fin de ligne sous la forme des deux octets Carriage
Return et Line Feed), d'une série
d'en-têtes ressemblant à celui de l'IMF du
RFC 5322 (par exemple Accept:
text/*
), d'une ligne vide et peut-être d'un corps du
message. Les requêtes du client au serveur et les réponses du
serveur au client sont toutes les deux des messages, la seule
différence étant que, pour la requête, la ligne de démarrage est une
ligne de requête et, pour la réponse, c'est une ligne d'état. (Le
RFC note qu'on pourrait réaliser un logiciel HTTP qui soit à la fois
serveur et client, distinguant requêtes et réponses d'après les
formats distincts de ces deux lignes. En pratique, personne ne
semble l'avoir fait.)
Pour une première démonstration de HTTP, on va utiliser le module http.server du langage Python, qui permet d'avoir un serveur HTTP opérationnel facilement :
% python3 -m http.server Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
Hop, nous avons un serveur HTTP qui tourne sur le
port 8000. On va utiliser
curl et son option -v
,
qui permet de voir le dialogue (le > indique ce qu'envoie curl,
le < ce qu'il reçoit du serveur en Python) :
% curl -v http://localhost:8000/ > GET / HTTP/1.1 > Host: localhost:8000 > User-Agent: curl/7.68.0 > Accept: */* > < HTTP/1.0 200 OK < Server: SimpleHTTP/0.6 Python/3.8.10 < Date: Thu, 06 Jan 2022 17:24:13 GMT < Content-type: text/html; charset=utf-8 < Content-Length: 660 < <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> ...
La ligne qui commence par GET
est la ligne de
démarrage, ici une requête, curl a envoyé trois lignes d'en-tête. La
ligne qui commence par HTTP/1.0
est la ligne de
démarrage de la réponse, et elle est suivie par quatre lignes
d'en-tête. La requête n'avait pas de corps, mais la réponse en a un
(il commence par <!DOCTYPE HTML PUBLIC
), ici
au format HTML. En dépit du H de son nom, HTTP n'a pas
grand'chose de spécifiquement lié à
l'hypertexte, et peut être utilisé pour tout
type de données (le serveur Indian ne renvoie que du texte
brut).
Pour le corps des messages, HTTP utilise certains concepts de MIME (RFC 2045). Mais HTTP n'est pas MIME : l'annexe B détaille les différences.
Le client est censé lire la réponse, commençant par la ligne
d'état, puis tout l'en-tête jusqu'à une ligne vide, puis le corps,
dont la taille est indiquée par le champ
Content-Length:
, ici 660 octets. (Sans ce
champ, le client va lire jusqu'à la fin de la connexion TCP sous-jacente.)
Notez qu'Indian ne fait pas cela bien : il fait une seule opération
de lecture et analyse ensuite le résultat (alors qu'il faudra
peut-être plusieurs opérations, et que, si on utilise les connexions
persistentes, on ne peut découvrir la fin du corps que si on tient
compte de Content-Length:
, ou des délimiteurs
de Transfer-Encxoding: chunked
). Ce choix a été
fait pour simplifier l'analyse syntaxique
(qui devrait normalement être incrémentale,
contrairement à ce que fait Indian, mais la bibliothèque
utilisée ne le permet pas, contrairement à, par exemple tree-sitter). Rappelez-vous que
ce n'est qu'un programme de démonstration.
Quand la réponse est du texte, le client ne doit pas supposer un encodage particulier, il doit lire des octets, quitte à les convertir dans des concepts de plus haut niveau (comme les caractères) plus tard.
Notez tout de suite qu'on trouve de tout dans le monde HTTP, et que beaucoup de clients et de serveurs ne suivent pas forcément rigoureusement la norme dans ses moindres détails. En général, Indian est plutôt strict et colle à la norme, sauf dans les cas où il était absolument nécessaire d'être plus tolérant pour pouvoir être testé avec les clients que j'ai utilisé. Comme souvent sur l'Internet, ces déviations par rapport à la norme permettent des attaques rigolotes comme le request smuggling (section 11.2 du RFC) ou le response splitting (section 11.1).
La réponse du serveur indique un numéro de version, sous la forme de deux chiffres séparés par un point. Ce RFC spécifie la version 1.1 de HTTP (Indian peut aussi gérer la version 1.0).
Commençons par la requête (section 3 du RFC). Elle commence par
une ligne qui comprend la méthode, le chemin et la version de
HTTP. Elles sont séparées par un espace. Pour analyser les requêtes,
Indian utilise la combinaison
d'analyseurs syntaxiques avec NimbleParsec, l'analyseur de la
requête est donc : method |> ignore(string(" ")) |>
concat(path) |> ignore(string(" ")) |>
concat(version)
. (La norme ne prévoit qu'un seul espace,
autrement, on aurait pu prévoir une répétition de string("
")
. Le RFC suggère que cette version plus laxiste est
acceptable mais peut être dangereuse.) La méthode indique ce
que le client veut faire à la ressource désignée. La plus connue des
méthodes est
GET
(récupérer la ressource) mais il en existe
d'autres, et la
liste peut changer. Indian ne met donc pas un choix limitatif
mais accepte tout nom de méthode (method =
ascii_string([not: ?\ ], min: 1)
), quitte à vérifier plus
tard. La ressource sur laquelle le client veut agir est indiquée par
un chemin (ou, dans certains cas par l'URL complet). Ainsi, un client qui
veut récupérer
http://www.example.org/truc?machin
va envoyer
au serveur au moins :
GET /truc?machin HTTP/1.1 Host: www.example.org
Il existe d'autres formes pour la requête mais je ne les présente pas ici (lisez le RFC).
La première ligne de la requête est suivie de l'en-tête, composée
de plusieurs champs (cf. section 5). Voici la requête que génère
wget pour récupérer
https://cis.cnrs.fr/a-travers-les-infrastructures-c-est-la-souverainete-numerique-des-etats-qui-se-joue/
:
% wget -d https://cis.cnrs.fr/a-travers-les-infrastructures-c-est-la-souverainete-numerique-des-etats-qui-se-joue/ ... GET /a-travers-les-infrastructures-c-est-la-souverainete-numerique-des-etats-qui-se-joue/ HTTP/1.1 User-Agent: Wget/1.20.3 (linux-gnu) Accept: */* Accept-Encoding: identity Host: cis.cnrs.fr Connection: Keep-Alive
Une particularité souvent oubliée de HTTP est qu'il n'y a pas de limite de taille à la plupart des éléments du protocole. Les programmeurs se demandent souvent « quelle place dois-je réserver pour tel élément ? » et la réponse est souvent qu'il n'y a pas de limite, juste des indications. Par exemple, notre RFC dit juste qu'il faut accepter des lignes de requête de 8 000 octets au moins.
Le serveur répond avec une ligne d'état et un autre en-tête (section 4). La ligne d'état comprend la version de HTTP, un code de retour formé de trois chiffres, et un message facultatif (là encore, avec un espace comme séparateur). Voici par exemple la réponse d'Indian :
HTTP/1.1 200 Content-Type: text/plain Content-Length: 18 Server: myBeautifulServerWrittenInElixir
Le message est d'autant plus facultatif (Indian n'en met pas) qu'il n'est pas forcément dans la langue du destinataire et qu'il n'est pas structuré, donc pas analysable. Le RFC recommande de l'ignorer.
Beaucoup plus important est le code de retour. Ces trois chiffres indiquent si tout s'est bien passé ou pas. Ils sont décrits en détail dans le RFC 9110, section 15. Bien qu'il s'agisse normalement d'éléments de protocole, certains sont bien connus des utilisatrices et utilisateurs, comme le célèbre 404. Et ils ont une représentation en chats et on a proposé de les remplacer par des émojis.
L'en-tête, maintenant (section 5 du RFC). Il se compose de
plusieurs lignes, chacune comportant le nom du champ, un
deux-points (pas d'espace avant ce
deux-points, insiste le RFC), puis la valeur du champ. Cela
s'analyse dans Indian avec header_line = header_name |>
ignore(string(":")) |> ignore(repeat(string(" "))) |>
concat(header_value) |> ignore(eol)
. Les noms de champs
possibles sont dans un
registre IANA (on peut noter qu'avant ce RFC, ils étaient
mêlés aux champs du courrier électronique dans un même registre).
Après les en-têtes, le corps. Il est en général absent des
requêtes faites avec la méthode GET
mais il est
souvent présent pour les autres méthodes, et il est en général dans
les réponses. Ici, une réponse d'un serveur avec le corps en
JSON :
% curl -v https://atlas.ripe.net/api/v2/measurements/34762605/results/ ... < HTTP/1.1 200 OK < Server: nginx < Date: Tue, 11 Jan 2022 20:19:31 GMT < Content-Type: application/json < Transfer-Encoding: chunked ... [{"fw":5020,"mver":"2.2.0","lts":4,"resultset":[{"time":1641657433,"lts":4,"subid":1,"submax":1,"dst_addr":"127.0.0.1","dst_port":"53","af":4,"src_addr":"127.0.0.1","proto":"UDP","result":{"rt":487.455,"size":127,"abuf":"9+SBgAABA ...
Le champ Content-Length:
est normalement
obligatoire dans la réponse, sauf s'il y a un champ
Transfer-Encoding:
, comme ici. Il permet au
client de gérer sa mémoire, et de savoir s'il a bien tout
récupéré. (Avec TLS, si on reçoit un signal de fin de
l'application, on sait qu'on a toute les données mais, sans TLS, on
ne pourrait pas être sûr, s'il n'y avait ce
Content-Length:
.)
HTTP/1.1 est un protocole simple (quoiqu'il y ait un certain nombre de pièges pour une mise en œuvre réelle) et on peut donc se contenter de telnet comme client HTTP :
% telnet evil.com 80 Trying 66.96.146.129... Connected to evil.com. Escape character is '^]'. GET / HTTP/1.1 Host: evil.com HTTP/1.1 200 OK Date: Sun, 16 Jan 2022 11:31:05 GMT Content-Type: text/html Content-Length: 4166 Connection: keep-alive Server: Apache/2 Last-Modified: Sat, 15 Jan 2022 23:21:33 GMT Accept-Ranges: bytes Cache-Control: max-age=3600 Etag: "1046-5d5a72e24309e" Expires: Sun, 16 Jan 2022 12:14:45 GMT Age: 980 <HTML> <HEAD> <meta content="Microsoft FrontPage 6.0" name="GENERATOR"> <meta content="FrontPage.Editor.Document" name="ProgId">
Les lignes GET / HTTP/1.1
et Host:
evil.com
ont été tapées à la main, une fois telnet
connecté. HTTP/1.1 (contrairement aux versions 2 et 3) fait partie
de ces protocoles en texte, qu'on peut déboguer à la main avec
telnet.
En plus perfectionné que telnet, il y a netcat :
% echo -n "GET /hello HTTP/1.1\r\nConnection: close\r\n\r\n" | nc ip6-localhost 8080 HTTP/1.1 200 Content-Type: text/plain Content-Length: 12 Server: myBeautifulServerWrittenInElixir Hello, ::1!
On a dit plus haut que HTTP/1.1 fonctionnait au-dessus d'une connexion TCP. La section 9 de notre RFC décrit la gestion de cette connexion. (En HTTP 0.9, c'était simple, une transaction = une connexion, mais ça a changé avec HTTP 1.) HTTP n'a pas forcément besoin de TCP (d'ailleurs, HTTP/3 fonctionne sur QUIC), il lui faut juste une liaison fiable faisant passer les octets dans l'ordre et sans perte. Dans HTTP/1.1, c'est TCP qui fournit ce service. (Avec TLS si on fait du HTTPS.) L'établissement d'une connexion TCP prend du temps, et la latence est un des plus gros ennemis de HTTP. Il est donc recommandé de ne pas établir une connexion TCP par transaction HTTP, mais de réutiliser les connexions. Le problème est délicat car le serveur peut avoir envie de supprimer des connexions pour récupérer des ressources. Clients et serveurs doivent donc s'attendre à des comportements variés de la part de leur partenaire.
HTTP/1 n'a pas d'identificateur de requête (comme
a, par exemple, le DNS). Les transactions doivent donc se faire
dans l'ordre : si on envoie une requête A puis une requête B sur la
même connexion TCP, on recevra forcément la réponse A puis la
B. (HTTP/2 et encore plus HTTP/3 ont par contre une certaine dose de
parallélisme.) Les connexions sont persistentes par défaut dans
HTTP/1.1 (ce n'était pas le cas en HTTP/1.0) et des champs de
l'en-tête servent à contrôler cette persistence
(Connection: close
indique qu'on ne gardera pas
la connexion ouverte, et un client poli qui ne fait qu'une requête
doit envoyer ce champ). Dans le code source d'Indian, les accès à
context["persistent-connection"]
vous
montreront la gestion de connexion.
Si le client et le serveur gère les connexions persistentes, le client peut aussi envoyer plusieurs requêtes à la suite, sans attendre les réponses (ce qu'on nomme le pipelining). Les réponses doivent parvenir dans le même ordre (puisqu'il n'y a pas d'identificateur de requête, qui permettrait de les associer à une requête), donc HTTP/1.1 ne permet pas un vrai parallélisme.
Pour économiser les ressources du serveur, un client ne devrait pas ouvrir « trop » de connexions vers un même serveur. (Le RFC 2616, section 8.1.4, mettait une limite de 2 connexions mais cette règle a disparu par la suite.)
Jusqu'à présent, on a parlé de HTTP tournant directement sur
TCP. Mais cela fait passer toutes les données en clair, ce qui est
inacceptable du point de vue sécurité, dans un monde de surveillance
massive. Aujourd'hui, la grande majorité des connexions HTTP passent
sur TLS, un
mécanisme cryptographique qui assure
notamment la confidentialité et
l'authentification du serveur. HTTPS (HTTP
sur TLS) était autrefois normalisé dans le RFC 2818 mais qui a désormais été abandonné au profit du RFC 9110 et de notre RFC 9112. Le principe
pour HTTP/1.1 est simple : une fois la connexion TCP établie, le
client HTTP démarre une session TLS (RFC 8446)
par dessus et voilà. (L'ALPN à utiliser est
http/1.1
.) Lors de la fermeture de la connexion, TLS envoie normalement
un message qui permet de différencier les coupures volontaires et
les pannes (close_notify
, RFC 8446, section 6.1). (Indian ne gère pas TLS, si on veut
le sécuriser - mais ce n'est qu'un programme de démonstration, il
faut le faire tourner derrière stunnel ou
équivalent.)
Pour tester HTTPS à la main, on peut utiliser un programme
distribué avec GnuTLS, ici pour récupérer https://fr.wikipedia.org/wiki/Hunga_Tonga
:
% gnutls-cli fr.wikipedia.org ... Connecting to '2620:0:862:ed1a::1:443'... - subject `CN=*.wikipedia.org,O=Wikimedia Foundation\, Inc.,L=San Francisco,ST=California,C=US', issuer `CN=DigiCert ... ... - Simple Client Mode: GET /wiki/Hunga_Tonga HTTP/1.1 Host: fr.wikipedia.org Connection: close HTTP/1.1 200 OK Date: Sun, 16 Jan 2022 20:41:56 GMT Content-Type: text/html; charset=UTF-8 Content-Length: 79568 ... <!DOCTYPE html> <html class="client-nojs" lang="fr" dir="ltr"> <head> <meta charset="UTF-8"/> <title>Hunga Tonga — Wikipédia</title> ...
Les trois lignes commençant par GET
ont été
tapées à la main par l'utilisateur.
La section 10 de notre RFC traite d'une fonction plus rare :
l'inclusion d'un message HTTP comme donnée d'un protocole (qui peut
être HTTP ou un autre). Un tel message est étiqueté avec le
type MIME application/http
.
Quelques mots sur la sécurité pour finir (section 11) : en raison de la complexité du protocole (qui est moins simple qu'il n'en a l'air !) et des mauvaises mises en œuvre qu'il faut quand même gérer car elles sont largement présentes sur le Web, deux programmes peuvent interpréter la même session HTTP différemment. Cela permet par exemple l'attaque de response splitting (cf. l'article de Klein « Divide and Conquer - HTTP Response Splitting, Web Cache Poisoning Attacks, and Related Topics »). Autre attaque possible, le request smuggling (cf. l'article de Linhart, Klein, Heled et Orrin, « HTTP Request Smuggling »).
Notre section 11 rappelle aussi que HTTP tout seul ne fournit pas de mécanisme pour assurer l'intégrité et la confidentialité des communications. Il dépend pour cela d'un protocole sous-jacent, en pratique TLS (HTTP+TLS étant appelé HTTPS).
L'annexe C décrit les changements de HTTP jusqu'à cette version
1.1. Ainsi, HTTP/1.0 a introduit la notion d'en-têtes, qui a permis,
entre autres, le virtual
hosting, grâce au champ
Host:
. HTTP/1.1 a notamment changé la
persistence par défaut des connexions (de non-persistente à
désormais persistente). Et notre RFC, par rapport à la précédente
norme de HTTP/1.1, le RFC 7230 ? Le plus gros
changement est éditorial, toutes les parties indépendantes du numéro
de version de HTTP ont été déplacées vers le RFC 9110, notre RFC ne gardant que ce qui est spécifique à
HTTP/1.1. S'il y a beaucoup de changements de détail, le protocole
n'est pas modifié, un client ou un serveur HTTP/1.1 reste
compatible.
Vous noterez que j'ai fait un cours HTTP au CNAM, dont les supports et la vidéo sont disponibles. HTTP/1 est un protocole simple, très simple, et trivial à programmer. Cela en fait un favori des enseignants en informatique car on peut écrire un client (ou même un serveur) HTTP très facilement, et il peut être utilisé contre des serveurs (ou des clients) réels, ce qui est motivant pour les étudiant·es.
Date de publication du RFC : Juin 2022
Auteur(s) du RFC : M. Bishop (Akamai)
Chemin des normes
Réalisé dans le cadre du groupe de travail IETF quic
Première rédaction de cet article le 7 juin 2022
Le protocole de transport QUIC, bien que permettant plusieurs protocoles applicatifs au-dessus de lui, a été surtout conçu pour HTTP. Il est donc logique que le premier protocole applicatif tournant sur QUIC et normalisé soit HTTP. Voici donc HTTP/3, la nouvelle version d'HTTP, et la première qui tourne sur QUIC, les précédentes tournant sur TCP (et, souvent, sur TLS sur TCP). À part l'utilisation de QUIC, HTTP/3 est très proche de HTTP/2.
Un petit point d'histoire sur HTTP : la version 1.1 (dont la norme précédente datait de 2014) utilise du texte, ce qui est pratique pour les humains (on peut se servir de netcat ou telnet comme client HTTP), mais moins pour les programmes. En outre, il n'a aucun multiplexage, encourageant les auteurs de navigateurs à ouvrir plusieurs connexions TCP simultanées avec le serveur, connexions qui ne partagent pas d'information entre elles et sont donc inefficaces. La version 2 de HTTP (RFC 9113) est binaire et donc plus efficace, et elle inclut du multiplexage. Par contre, les limites de TCP (une perte de paquet va affecter toutes les ressources en cours de transfert, une ressource lente peut bloquer une rapide, etc) s'appliquent toujours. D'où le passage à QUIC pour HTTP/3. QUIC améliore la latence, notamment grâce à la fusion avec TLS, et fournit du « vrai » multiplexage. Autrement, HTTP/3 est très proche de HTTP/2, mais il est plus simple, puisque le multiplexage est délégué à la couche transport.
QUIC offre en effet plusieurs fonctions très pratiques pour HTTP/3, notamment le multiplexage (complet : plus de head-of-line blocking), le contrôle de débit par ruisseau et pas par connexion entière, et l'établissement de connexion à faible latence. QUIC a été conçu en pensant à HTTP et aux problèmes spécifiques du Web.
Nous avons maintenant toute une panoplie de versions de HTTP, du HTTP/1.1 du RFC 9112 et suivants, au HTTP/2 du RFC 9113, puis désormais à notre HTTP/3. De même que HTTP/2 n'a pas supprimé HTTP/1, HTTP/3 ne supprimera pas HTTP/2, ne serait-ce que parce qu'il existe encore beaucoup de réseaux mal gérés et/ou restrictifs, où QUIC ne passe pas. Toutes ces versions de HTTP ont en commun les mêmes sémantiques, décrites dans le RFC 9110.
La section 2 du RFC fait un panorama général de HTTP/3. Quand le
client HTTP sait (on verra plus tard comment il peut savoir) que le
serveur fait du HTTP/3, il ouvre une connexion QUIC avec ce
serveur. QUIC fera le gros du travail. À l'intérieur de chaque
ruisseau QUIC, il y aura un seul couple requête/réponse HTTP. La
requête et la réponse seront placées dans des trames
QUIC de type STREAM
(les trames de
contenu dans QUIC). À l'intérieur des trames QUIC, il y aura des
trames
HTTP, dont deux types sont particulièrement importants,
HEADERS
et DATA
. QUIC
étant ce qu'il est, chaque couple requête/réponse est séparé et, par
exemple, la lenteur à envoyer une réponse n'affectera pas les autres
requêtes, mêmes envoyées après celle qui a du mal à avoir une
réponse. (Il y a aussi la possibilité pour le serveur d'envoyer des
données spontanément, avec les trames HTTP de type
PUSH_PROMISE
et quelques autres.) Les requêtes
et les réponses ont un encodage binaire comme en HTTP/2, et sont
comprimées avec QPACK (RFC 9114), alors que HTTP/2 utilisait HPACK (qui nécessitait
une transmission ordonnées des octets, qui n'existe plus dans une
connexion QUIC).
Après cette rapide présentation, voyons les détails, en
commençant par le commencement, l'établissement des connexions entre
client et serveur. D'abord, comment savoir si le serveur veut bien
faire du HTTP/3 ? Le client HTTP a reçu consigne de l'utilisateur
d'aller en
https://serveur-pris-au-hasard.example/
,
comment va t-il choisir entre HTTP/3 et des versions plus classiques
de HTTP ? Il n'y a rien dans l'URL qui indique que QUIC est possible mais il y
a plusieurs méthodes de découverte, permettant au client une grande
souplesse. D'abord, le client peut simplement tenter sa chance : on
ouvre une connexion QUIC vers le port 443 et
on voit bien si ça marche ou si on reçoit un message ICMP nous
disant que c'est raté. Ici, un exemple vu avec
tcpdump :
11:07:20.368833 IP6 (hlim 64, next-header UDP (17) payload length: 56) 2a01:e34:ec43:e1d0:554:492d:1a13:93e4.57926 > 2001:41d0:302:2200::180.443: [udp sum ok] UDP, length 48 11:07:20.377878 IP6 (hlim 52, next-header ICMPv6 (58) payload length: 104) 2001:41d0:302:2200::180 > 2a01:e34:ec43:e1d0:554:492d:1a13:93e4: [icmp6 sum ok] ICMP6, destination unreachable, unreachable port, 2001:41d0:302:2200::180 udp port 443
Si ça rate, on se rabat en une version de HTTP sur TCP (les premiers tests menés par Google indiquaient qu'entre 90 et 95 % des utilisateurs avaient une connectivité UDP correcte et pouvaient donc utiliser QUIC). Mais le cas ci-dessus était le cas idéal où on avait reçu le message ICMP et où on avait pu l'authentifier. Comme il est possible qu'un pare-feu fasciste et méchant se trouve sur le trajet, et jette silencieusement les paquets UDP, sans qu'on reçoive de réponse, même négative, il faut que le client soit plus intelligent que cela, et essaie très vite une autre version de HTTP, suivant le principe des globes oculaires heureux (RFC 8305). L'Internet est en effet farci de middleboxes qui bloquent tout ce qu'elles ne connaissent pas, et le client HTTP ne peut donc jamais être sûr qu'UDP passera. C'est même parfois un conseil explicite de certains vendeurs.
Sinon, le serveur
peut indiquer explicitement qu'il gère HTTP/3 lors d'une connexion
avec une vieille version de HTTP, via l'en-tête
Alt-Svc:
(RFC 7838). Le
client essaie d'abord avec une vieille version de HTTP, puis est
redirigé, et se souviendra ensuite de la redirection. Par exemple,
si la réponse HTTP contient :
Alt-Svc: h3=":443"
Alors le client sait qu'il peut essayer HTTP/3 sur le port
443. Il peut y avoir plusieurs services alternatifs dans un
Alt-Svc:
(par exemple les versions
expérimentales de HTTP/3).
Dernière possibilité, celle décrite dans le RFC 8164. (Par contre, l'ancien mécanisme
Upgrade:
et sa réponse 101 n'est plus utilisé
par HTTP/3.)
À propos de port, j'ai cité jusqu'à
présent le 443 car c'est le port par défaut, mais on peut évidemment
en utiliser un autre, en l'indiquant dans
l'URL, ou via
Alt-Svc:
. Quant aux URL de plan
http:
(tout court, sans le S), ils ne sont pas
utilisables directement (puisque QUIC n'a pas de mode en clair, TLS
est obligatoire) mais peuvent quand même rediriger vers du HTTP/3,
via Alt-Svc:
.
Le client a donc découvert le serveur, et il se connecte. Il doit
utiliser l'ALPN TLS (RFC 7301,
qui est quasi-obligatoire avec QUIC, et indiquer comme application
h3
(cf. le registre
IANA des applications). Les réglages divers qui
s'appliqueront à toute la connexion (la liste des réglages possibles
est dans un
registre IANA) sont envoyés dans une trame HTTP de type
SETTINGS
. La connexion QUIC peut évidemment
rester ouverte une fois les premières requêtes envoyées et les
premières réponses reçues, afin d'amortir le coût de connexion sur
le plus grand nombre de requêtes possible. Évidemment, le serveur
est autorisé à couper les connexions qui lui semblent inactives (ce
qui se fait normalement en envoyant une trame HTTP de type
GOAWAY
), le client doit donc être prêt à les
réouvrir.
Voilà pour la gestion de connexions. Et, une fois qu'on est
connecté, comment se font les requêtes (section 4 du RFC) ? Pour
chaque requête, on envoie une trame HTTP de type
HEADERS
contenant les en-têtes HTTP (encodés,
je le rappelle, en binaire) et la méthode utilisée
(GET
, POST
, etc), puis une
trame de type DATA
si la requête contient des
données. Puis on lit la réponse envoyée par le serveur. Le ruisseau
est fermé ensuite, chaque ruisseau ne sert qu'à un seul couple
requête/réponse. (Rappelez-vous que, dans QUIC, envoyer une trame
QUIC de type STREAM
suffit à créer le ruisseau
correspondant. Tout ce qui nécessite un état a été fait lors de la
création de la connexion QUIC.)
Comme expliqué plus haut, les couples requête/réponse se font sur un ruisseau, qui ne sert qu'une fois. Ce ruisseau est bidirectionnel (section 6), ce qui permet de corréler facilement la requête et la réponse : elles empruntent le même ruisseau. C'est celui de numéro zéro dans l'exemple plus loin, qui n'a qu'un seul couple requête/réponse. La première requête se fera toujours sur le ruisseau 0, les autres seront que les ruisseaux 4, 8, etc, selon les règles de génération des numéros de ruisseau de QUIC.
Voici,
un exemple, affiché par tshark d'un échange HTTP/3. Pour
pouvoir le déchiffrer, on a utilisé la méthode classique, en
définissant la variable
d'environnement SSLKEYLOGFILE
avant
de lancer le client HTTP (ici, curl), puis en disant à Wireshark d'utiliser
ce fichier contenant la clé (tls.keylog_file:
/tmp/quic.key
dans
~/.config/wireshark/preferences
). Cela donne :
% tshark -n -r /tmp/quic.pcap 1 0.000000 10.30.1.1 → 45.77.96.66 QUIC 1294 Initial, DCID=94b8a6888cb47e3128b13d875980b557d9e415f0, SCID=57e2ce80152d30c1f66a9fcb3c3b49c81c98f329, PKN: 0, CRYPTO, PADDING 2 0.088508 45.77.96.66 → 10.30.1.1 QUIC 1242 Handshake, DCID=57e2ce80152d30c1f66a9fcb3c3b49c81c98f329, SCID=3bd5658cf06b1a5020d44410ab9682bcf277610f, PKN: 0, CRYPTO[Malformed Packet] 3 0.088738 10.30.1.1 → 45.77.96.66 QUIC 185 Handshake, DCID=3bd5658cf06b1a5020d44410ab9682bcf277610f, SCID=57e2ce80152d30c1f66a9fcb3c3b49c81c98f329, PKN: 0, ACK 4 0.089655 45.77.96.66 → 10.30.1.1 QUIC 1239 Handshake, DCID=57e2ce80152d30c1f66a9fcb3c3b49c81c98f329, SCID=3bd5658cf06b1a5020d44410ab9682bcf277610f, PKN: 1, CRYPTO 5 0.089672 10.30.1.1 → 45.77.96.66 QUIC 114 Handshake, DCID=3bd5658cf06b1a5020d44410ab9682bcf277610f, SCID=57e2ce80152d30c1f66a9fcb3c3b49c81c98f329, PKN: 1, ACK, PING 6 0.090740 45.77.96.66 → 10.30.1.1 QUIC 931 Handshake, DCID=57e2ce80152d30c1f66a9fcb3c3b49c81c98f329, SCID=3bd5658cf06b1a5020d44410ab9682bcf277610f, PKN: 2, CRYPTO 7 0.091100 10.30.1.1 → 45.77.96.66 HTTP3 257 Protected Payload (KP0), DCID=3bd5658cf06b1a5020d44410ab9682bcf277610f, PKN: 0, NCI, STREAM(2), SETTINGS, STREAM(10), STREAM(6) 8 0.091163 10.30.1.1 → 45.77.96.66 HTTP3 115 Protected Payload (KP0), DCID=3bd5658cf06b1a5020d44410ab9682bcf277610f, PKN: 1, STREAM(0), HEADERS 9 0.189511 45.77.96.66 → 10.30.1.1 HTTP3 631 Protected Payload (KP0), DCID=57e2ce80152d30c1f66a9fcb3c3b49c81c98f329, PKN: 0, ACK, DONE, CRYPTO, STREAM(3), SETTINGS 10 0.190684 45.77.96.66 → 10.30.1.1 HTTP3 86 Protected Payload (KP0), DCID=57e2ce80152d30c1f66a9fcb3c3b49c81c98f329, PKN: 1, STREAM(7) 11 0.190792 10.30.1.1 → 45.77.96.66 QUIC 85 Protected Payload (KP0), DCID=3bd5658cf06b1a5020d44410ab9682bcf277610f, PKN: 2, ACK 12 0.192047 45.77.96.66 → 10.30.1.1 HTTP3 86 Protected Payload (KP0), DCID=57e2ce80152d30c1f66a9fcb3c3b49c81c98f329, PKN: 2, STREAM(11) 13 0.193299 45.77.96.66 → 10.30.1.1 HTTP3 604 Protected Payload (KP0), DCID=57e2ce80152d30c1f66a9fcb3c3b49c81c98f329, PKN: 3, STREAM(0), HEADERS, DATA 14 0.193421 10.30.1.1 → 45.77.96.66 QUIC 85 Protected Payload (KP0), DCID=3bd5658cf06b1a5020d44410ab9682bcf277610f, PKN: 3, ACK
On y voit au début l'ouverture de la connexion QUIC. Puis, à partir
du datagramme 7 commence HTTP/3, avec la création des ruisseaux
nécessaires et l'envoi de la trame SETTINGS
(ruisseau 3) et de HEADERS
(ruisseau 0). Pas de
trame DATA
, c'était une simple requête HTTP
GET
, sans corps. Le serveur répond par son
propre SETTINGS
, une trame
HEADERS
et une DATA
(la
réponse est de petite taille et tient dans un seul datagramme). Vous
avez l'analyse complète et détaillée de cette session QUIC dans le fichier
http3-get-request.txt
.
La section 7 décrit les différents types de trames définis pour
HTTP/3 (rappelez-vous que ce ne sont pas les mêmes que les trames
QUIC : toutes voyagent dans des trames QUIC de type
STREAM
), comme :
HEADERS
(type 1), qui transporte les
en-têtes HTTP, ainsi que les « pseudo en-têtes » comme la méthode
HTTP. Ils sont comprimés avec QPACK (RFC 9114).DATA
(type 0) qui contient le corps des
messages. Une requête GET
ne sera sans doute
pas accompagnée de ce type de trames mais la réponse aura, dans la
plupart des cas, un corps et donc une ou plusieurs trames de type
DATA
.SETTINGS
(type 4), qui définit des
réglages communs à toute la connexion. Les réglages possibles
figurent dans un
registre IANA.GOAWAY
(type 7) sert à dire qu'on s'en va.Les différents types de trames QUIC sont dans un registre IANA. Ce registre est géré selon les politiques (cf. RFC 8126) « action de normalisation » pour une partie, et « spécification nécessaire » pour une autre partie de l'espace des types, plus ouverte. Notez que les types de trame HTTP/3 ne sont pas les mêmes que ceux de HTTP/2, même s'ils sont très proches. Des plages de types sont réservées (section 7.2.8) pour le graissage, l'utilisation délibérée de types non existants pour s'assurer que le partenaire respecte bien la norme, qui commande d'ignorer les types de trame inconnus. (Le graissage et ses motivations sont expliqués dans le RFC 8701.)
Wireshark peut aussi analyser graphiquement les données et vous
pouvez voir ici une trame QUIC de type STREAM
(ruisseau 0) contenant une trame HTTP/3 de type
HEADERS
. Notez que cette version de Wireshark
ne décode pas encore la requête HTTP (ici, un simple
GET
) :
Et une fois qu'on a terminé ? On ferme la connexion (section 5), ce qui peut arriver pour plusieurs raisons, par exemple :
GOAWAY
,Bien sûr, des tas de choses peuvent aller mal pendant une connexion HTTP/3. La section 8 du RFC définit donc un certain nombre de codes d'erreurs pour signaler les problèmes. Ils sont stockés dans un registre IANA.
L'un des buts essentiels de QUIC était d'améliorer la
sécurité. La section 10 du RFC discute de ce qui a été fait. En
gros, la sécurité de HTTP/3 est celle de HTTP/2 quand il est combiné
avec TLS, mais il y a quelques points à garder en tête. Par exemple,
HTTP/3 a des fonctions, comme la compression d'en-têtes (RFC 9114) ou comme le
contrôle de flux de chaque ruisseau qui, utilisées sans précaution,
peuvent mener à une importante allocation de ressources. Les
réglages définis dans la trame SETTINGS
servent
entre autres à mettre des limites strictes à la consommation de
ressources.
Autre question de sécurité, liée cette fois à la protection de la
vie privée, la taille des
données. Par défaut, TLS ne cherche pas à dissimuler la taille des
paquets. Si on ne sait pas quelle page a chargé un client HTTPS,
l'observation de la taille des données reçues, comparée à ce qu'on
obtient en se connectant soi-même au serveur, permet de trouver avec
une bonne probabilité les ressources demandées (c'est ce qu'on nomme
l'analyse de trafic). Pour contrer cela, on peut utiliser le
remplissage. HTTP/3 peut utiliser celui de
QUIC (avec les trames QUIC de type PADDING
) ou
bien faire son propre remplissage avec des trames HTTP utilisant des
types non alloués, mais dont l'identificateur est réservé (les
trames HTTP de type inconnu doivent être ignorées par le
récepteur).
Toujours question sécurité, l'effort de QUIC et de HTTP/3 pour diminuer la latence, avec notamment la possibilité d'envoyer des données dès le premier paquet (early data), a pour conséquences de faciliter les attaques par rejeu. Il faut donc suivre les préconisations du RFC 8470.
La possibilité qu'offre QUIC de faire migrer une session d'une
adresse IP vers une autre (par exemple quand un ordiphone passe de
4G en WiFi ou réciproquement) soulève également des questions de
sécurité. Ainsi, journaliser l'adresse IP du
client (le access_log
d'Apache…) n'aura plus forcément le même
sens, cette adresse pouvant changer en cours de route. Idem pour les
ACL.
Ah, et question vie privée, le fait que HTTP/3 et QUIC encouragent à utiliser une seule session pour un certain nombre de requêtes peut permettre de corréler entre elles des requêtes. Sans cookie, on a donc une traçabilité des utilisateurs.
HTTP/3 a beaucoup de ressemblances avec HTTP/2. L'annexe A de
notre RFC détaille les différences entre ces deux versions et
explique comment passer de l'une à l'autre. Ainsi, la gestion des
ruisseaux qui, en HTTP/2, était faite par HTTP, est désormais faite
par QUIC. Une des conséquences est que l'espace des identificateurs
de ruisseau est bien plus grand, limitant le risque qu'on tombe à
cours. HTTP/3 a moins de types de trames que HTTP/2 car une partie
des fonctions assurées par HTTP/2 le sont par QUIC et échappent donc
désormais à HTTP (comme PING
ou WINDOW_UPDATE
).
HTTP/3 est en plein déploiement actuellement et vos logiciels
favoris peuvent ne pas encore l'avoir, ou bien n'avoir qu'une
version expérimentale de HTTP/3 et encore pas par défaut. Par
exemple, pour Google
Chrome, il faut le lancer google-chrome
--enable-quic --quic-version=h3-24
(h3-24
étant une version de développement de
HTTP/3). Pour Firefox, vous pouvez suivre
cet article ou celui-ci. Quand vous lirez ces lignes, tout sera
peut-être plus simple, avec le HTTP/3 officiel dans beaucoup de
clients HTTP.
J'ai montré plus haut quelques essais avec curl. Pour avoir HTTP/3 avec curl, il faut actuellement quelques bricolages, HTTP/3 n'étant pas forcément compilé dans le curl que vous utilisez. Déjà, il faut utiliser un OpenSSL spécial, disponible ici. Sinon, vous aurez des erreurs à la compilation du genre :
openssl.c:309:7: warning: implicit declaration of function ‘SSL_provide_quic_data’ [-Wimplicit-function-declaration] 309 | if (SSL_provide_quic_data(ssl, from_ngtcp2_level(crypto_level), data, | ^~~~~~~~~~~~~~~~~~~~~
ngtcp2 peut se compiler avec GnuTLS et pas le
OpenSSL spécial, mais curl ne l'accepte pas (configure:
error: --with-ngtcp2 was specified but could not find
ngtcp2_crypto_openssl pkg-config file.
). Ensuite, il
faut les bibliothèques ngtcp2 et nghttp3. Une fois
celles-ci prêtes, vous configurez curl :
% ./configure --with-ngtcp2 --with-nghttp3 ... HTTP2: enabled (nghttp2) HTTP3: enabled (ngtcp2 + nghttp3) ...
Vérifiez bien que vous avez la ligne HTTP3
indiquant que HTTP3 est activé. De même, un curl
--version
doit vous afficher HTTP3
dans les protocoles gérés. Vous pourrez enfin faire du HTTP/3 :
% curl -v --http3 https://quic.tech:8443 * Trying 45.77.96.66:8443... * Connect socket 5 over QUIC to 45.77.96.66:8443 * Connected to quic.tech () port 8443 (#0) * Using HTTP/3 Stream ID: 0 (easy handle 0x55879ffd4c40) > GET / HTTP/3 > Host: quic.tech:8443 > user-agent: curl/7.76.1 > accept: */* > * ngh3_stream_recv returns 0 bytes and EAGAIN * ngh3_stream_recv returns 0 bytes and EAGAIN * ngh3_stream_recv returns 0 bytes and EAGAIN < HTTP/3 200 < server: quiche < content-length: 462 < <!DOCTYPE html> <html> ...
Pour davantage de lecture sur HTTP/3 :
Si vous utilisez Tor, notez que QUIC et HTTP/2 (j'ai bien dit HTTP/2, puisque Tor ne gère pas UDP et donc pas QUIC) peuvent mettre en cause la protection qu'assure Tor. Une analyse sommaire est disponible En gros, si QUIC, grâce à son chiffrement systématique, donne moins d'infos au réseau, il en fournit peut-être davantage au serveur.
Ah, sinon j'ai présenté HTTP/3
à ParisWeb et
les supports sont disponibles en parisweb2021-http3-bortzmeyer.pdf
. Et Le blog de curl est désormais accessible
en HTTP/3. Voici d'autres parts quelques articles
intéressants annonçant la publication de HTTP/3 :
Articles des différentes années : 2022 2021 2020 2019 2018 2017 2016 Précédentes années
Syndication : Flux Atom avec seulement les résumés et Flux Atom avec tout le contenu.