Aller au contenu principal

8. 0-RTT and Anti-Replay (0-RTT et Protection Anti-Rejeu)

Comme indiqué dans la section 2.3 et l'annexe E.5, TLS ne fournit pas de protection anti-rejeu inhérente pour les données 0-RTT. Il existe deux menaces potentielles à prendre en compte :

  • Les attaquants réseau qui montent une attaque par rejeu en dupliquant simplement un envoi de données 0-RTT.

  • Les attaquants réseau qui exploitent le comportement de nouvelle tentative du client pour faire en sorte que le serveur reçoive plusieurs copies d'un message d'application. Cette menace existe déjà dans une certaine mesure car les clients qui valorisent la robustesse répondent aux erreurs réseau en tentant de réessayer les requêtes. Cependant, 0-RTT ajoute une dimension supplémentaire pour tout système serveur qui ne maintient pas d'état serveur globalement cohérent. Plus précisément, si un système serveur a plusieurs zones où les tickets de la zone A ne seront pas acceptés dans la zone B, alors un attaquant peut dupliquer un ClientHello et des données précoces destinés à A vers A et B. À A, les données seront acceptées en 0-RTT, mais à B le serveur rejettera les données 0-RTT et forcera plutôt une poignée de main complète. Si l'attaquant bloque le ServerHello de A, alors le client complétera la poignée de main avec B et réessaiera probablement la requête, conduisant à une duplication sur le système serveur dans son ensemble.

La première classe d'attaque peut être évitée en partageant l'état pour garantir que les données 0-RTT sont acceptées au plus une fois. Les serveurs DEVRAIENT (SHOULD) fournir ce niveau de protection anti-rejeu en implémentant l'une des méthodes décrites dans cette section ou une méthode équivalente. Il est cependant entendu qu'en raison de préoccupations opérationnelles, tous les déploiements ne maintiendront pas l'état à ce niveau. Par conséquent, en fonctionnement normal, les clients ne sauront pas laquelle, le cas échéant, de ces mécanismes les serveurs implémentent réellement et doivent donc UNIQUEMENT (MUST) envoyer des données précoces qu'ils jugent sûres d'être rejouées.

En plus des effets directs des rejeux, il existe une classe d'attaques où même les opérations normalement considérées comme idempotentes pourraient être exploitées par un grand nombre de rejeux (attaques temporelles, épuisement des limites de ressources et autres, comme décrit dans l'annexe E.5). Celles-ci peuvent être atténuées en garantissant que chaque charge utile 0-RTT ne peut être rejouée qu'un nombre limité de fois. Le serveur DOIT (MUST) s'assurer que toute instance de celui-ci (qu'il s'agisse d'une machine, d'un thread ou de toute autre entité au sein de l'infrastructure de service concernée) accepte 0-RTT pour la même poignée de main 0-RTT au plus une fois ; cela limite le nombre de rejeux au nombre d'instances de serveur dans le déploiement. Une telle garantie peut être accomplie en enregistrant localement les données des ClientHello récemment reçus et en rejetant les répétitions, ou par toute autre méthode qui fournit la même garantie ou une garantie plus forte. La garantie "au plus une fois par instance de serveur" est une exigence minimale ; les serveurs DEVRAIENT (SHOULD) limiter davantage le rejeu 0-RTT lorsque cela est possible.

La deuxième classe d'attaque ne peut pas être évitée au niveau de la couche TLS et DOIT (MUST) être traitée par toute application. Notez que toute application dont les clients implémentent un comportement de nouvelle tentative quelconque doit déjà implémenter une forme de défense anti-rejeu.

8.1. Single-Use Tickets (Tickets à Usage Unique)

La forme la plus simple de défense anti-rejeu consiste pour le serveur à n'autoriser chaque ticket de session à être utilisé qu'une seule fois. Par exemple, le serveur peut maintenir une base de données de tous les tickets valides en attente, supprimant chaque ticket de la base de données lors de son utilisation. Si un ticket inconnu est fourni, le serveur reviendrait alors à une poignée de main complète.

Si le ticket est autonome, plutôt que d'être une clé de base de données, et que le PSK correspondant est supprimé lors de l'utilisation, alors les connexions établies à l'aide de ce PSK bénéficient de la confidentialité persistante. Cela améliore la sécurité de toutes les données 0-RTT et de l'utilisation de PSK lorsque PSK est utilisé sans (EC)DHE.

Parce que ce mécanisme nécessite le partage de la base de données de session entre les nœuds serveur dans les environnements avec plusieurs serveurs distribués, il peut être difficile d'atteindre une haute disponibilité et des performances pour les connexions 0-RTT basées sur PSK par rapport aux tickets auto-chiffrés. Contrairement aux bases de données de session, les tickets de session peuvent réussir l'établissement de session basé sur PSK même sans stockage cohérent, bien qu'ils nécessitent toujours un stockage cohérent pour l'anti-rejeu des données 0-RTT, comme détaillé dans les sections suivantes.

8.2. Client Hello Recording (Enregistrement du Client Hello)

Une autre forme d'anti-rejeu consiste à enregistrer une valeur unique dérivée du ClientHello (généralement soit la valeur aléatoire, soit le lieur PSK) et à rejeter les doublons. L'enregistrement de tous les ClientHello fait croître l'état sans limite, mais un serveur peut à la place enregistrer les ClientHello pour une fenêtre temporelle donnée et utiliser le "obfuscated_ticket_age" pour s'assurer que les tickets ne sont pas réutilisés en dehors de cette fenêtre.

Pour implémenter ceci, lorsqu'un ClientHello est reçu, le serveur vérifie d'abord le lieur PSK comme décrit dans la section 4.2.11. Il calcule ensuite le expected_arrival_time comme décrit dans la section suivante et, s'il est en dehors de la fenêtre d'enregistrement, rejette 0-RTT, revenant à la poignée de main 1-RTT.

Si le expected_arrival_time est dans la fenêtre, alors le serveur vérifie s'il a enregistré un ClientHello correspondant. Si un est trouvé, il soit abandonne la poignée de main avec une alerte "illegal_parameter", soit accepte le PSK mais rejette 0-RTT. Si aucun ClientHello correspondant n'est trouvé, alors il accepte 0-RTT puis stocke le ClientHello tant que le expected_arrival_time est à l'intérieur de la fenêtre. Les serveurs PEUVENT (MAY) également implémenter des magasins de données avec des faux positifs, tels que les filtres de Bloom, auquel cas ils DOIVENT (MUST) répondre aux rejeux apparents en rejetant 0-RTT mais NE DOIVENT PAS (MUST NOT) abandonner la poignée de main.

Le serveur DOIT (MUST) dériver la clé de stockage uniquement à partir des sections validées du ClientHello. Si le ClientHello contient plusieurs identités PSK, alors un attaquant peut créer plusieurs ClientHello avec des valeurs de lieur différentes pour l'identité moins préférée en supposant que le serveur ne la vérifiera pas (comme recommandé par la section 4.2.11). C'est-à-dire, si le client envoie les PSK A et B mais que le serveur préfère A, alors l'attaquant peut changer le lieur pour B sans affecter le lieur pour A. Si le lieur pour B fait partie de la clé de stockage, alors ce ClientHello n'apparaîtra pas comme un doublon, ce qui fera accepter le ClientHello et peut causer des effets secondaires tels que la pollution du cache de rejeu, bien que toutes les données 0-RTT ne seront pas déchiffrées car elles utiliseront des clés différentes. Si le lieur validé ou le ClientHello.random est utilisé comme clé de stockage, alors cette attaque n'est pas possible.

Parce que ce mécanisme n'exige pas de stocker tous les tickets en attente, il peut être plus facile à implémenter dans les systèmes distribués avec un taux élevé de reprise et de 0-RTT, au prix d'une défense anti-rejeu potentiellement plus faible en raison de la difficulté de stocker et de récupérer de manière fiable les messages ClientHello reçus. Dans de nombreux systèmes de ce type, il est peu pratique de stocker tous les ClientHello reçus pour un système de stockage globalement cohérent. Dans ce cas, la meilleure protection anti-rejeu consiste à avoir une zone de stockage unique faisant autorité pour un ticket donné et à rejeter 0-RTT pour ce ticket dans toute autre zone. Cette approche empêche le rejeu simple par l'attaquant car une seule zone acceptera les données 0-RTT. Une conception plus faible consiste à implémenter un stockage séparé pour chaque zone mais à autoriser 0-RTT dans n'importe quelle zone. Cette approche limite le nombre de rejeux à une fois par zone. La duplication des messages d'application reste bien sûr possible avec l'une ou l'autre conception.

Lorsque les implémentations sont fraîchement démarrées, elles DEVRAIENT (SHOULD) rejeter 0-RTT tant qu'une partie de leur fenêtre d'enregistrement chevauche le temps de démarrage. Sinon, elles courent le risque d'accepter des rejeux qui ont été envoyés à l'origine pendant cette période.

Note : Si l'horloge du client fonctionne beaucoup plus rapidement que celle du serveur, alors un ClientHello peut être reçu qui est en dehors de la fenêtre dans le futur, auquel cas il pourrait être accepté pour 1-RTT, causant une nouvelle tentative du client, puis acceptable plus tard pour 0-RTT. Ceci est une autre variante de la deuxième forme d'attaque décrite dans la section 8.

8.3. Freshness Checks (Vérifications de Fraîcheur)

Parce que le ClientHello indique l'heure à laquelle le client l'a envoyé, il est possible de déterminer efficacement si un ClientHello pourrait avoir été rejoué récemment et d'accepter 0-RTT uniquement pour un tel ClientHello, revenant sinon à une poignée de main 1-RTT. Ceci est nécessaire pour le mécanisme de stockage ClientHello décrit dans la section 8.2 car sinon le serveur devrait stocker un nombre illimité de ClientHello, et est une optimisation utile pour les tickets à usage unique autonomes car cela permet le rejet efficace des ClientHello qui ne peuvent pas être utilisés pour 0-RTT.

Pour implémenter ce mécanisme, un serveur doit stocker l'heure à laquelle un ticket de session a été créé avec une estimation de décalage du temps d'aller-retour entre le client et le serveur. C'est-à-dire :

adjusted_creation_time = creation_time + estimated_RTT

Cette valeur peut être encodée dans le ticket, évitant ainsi la nécessité de conserver l'état pour chaque ticket en attente. Le serveur peut déterminer la vue du client de l'âge du ticket en soustrayant la valeur "ticket_age_add" du ticket du paramètre "obfuscated_ticket_age" dans l'extension "pre_shared_key" du client. Le serveur peut déterminer le expected_arrival_time du ClientHello comme suit :

expected_arrival_time = adjusted_creation_time + clients_ticket_age

Lorsqu'un nouveau ClientHello est reçu, le expected_arrival_time est comparé à l'heure actuelle de l'horloge murale du serveur et s'ils diffèrent de plus d'un certain montant, 0-RTT est rejeté, bien que la poignée de main 1-RTT puisse être complétée.

Il existe plusieurs sources potentielles d'erreur qui pourraient causer des décalages entre le expected_arrival_time et le temps mesuré. Les variations des taux d'horloge du client et du serveur sont susceptibles d'être minimes, bien que potentiellement les temps absolus puissent être décalés de grandes valeurs. Les délais de propagation réseau sont les causes les plus probables d'un décalage dans les valeurs légitimes du temps écoulé. Les messages NewSessionTicket et ClientHello pourraient tous deux être retransmis et donc retardés, ce qui pourrait être masqué par TCP. Pour les clients sur Internet, cela implique des fenêtres de l'ordre de dix secondes pour tenir compte des erreurs dans les horloges et des variations dans les mesures ; d'autres scénarios de déploiement peuvent avoir des besoins différents. Les distributions de décalage d'horloge ne sont pas symétriques, donc le compromis optimal peut impliquer une plage asymétrique de valeurs de décalage permises.

Notez que la vérification de fraîcheur seule n'est pas suffisante pour empêcher les rejeux car elle ne les détecte pas pendant la fenêtre d'erreur, qui—selon la bande passante et la capacité du système—pourrait inclure des milliards de rejeux dans des contextes réels. De plus, cette vérification de fraîcheur n'est effectuée qu'au moment où le ClientHello est reçu et non lorsque les enregistrements de données d'application précoces ultérieurs sont reçus. Après l'acceptation des données précoces, les enregistrements peuvent continuer à être diffusés vers le serveur sur une période plus longue.