Le protocole TLS 1.3 protège la confidentialité des échanges réseau contre l'interception sur les infrastructures intermédiaires. Pourtant, au-delà du chiffrement du flux applicatif, une vulnérabilité subsiste au niveau des métadonnées : le serveur de destination observe systématiquement l'adresse IP source du client.

Dans les usages conventionnels, cette visibilité constitue le fondement du routage IP. Cependant, lorsqu'il s'agit d'opérations sensibles — telles que la transmission de requêtes vers des modèles d'intelligence artificielle (LLM), l'envoi de rapports de télémétrie, la vérification biométrique ou la géolocalisation —, l'adresse IP fait office d'identifiant numérique durable. En croisant les horodatages et les adresses réseau, les serveurs d'application peuvent établir des profils d'activité comportementale détaillés, même lorsque l'utilisateur n'est connecté à aucun compte.

Les mandataires classiques (serveurs mandataires HTTP ou relais SOCKS5) ne font que déplacer le point de confiance : l'opérateur du proxy a la visibilité complète sur l'adresse de l'utilisateur et sur sa destination. Pour rompre cette corrélation sans exiger de confiance aveugle, l'IETF a standardisé la RFC 9458 (Oblivious HTTP, OHTTP), combinée à la RFC 9180 (Hybrid Public Key Encryption, HPKE) et à la RFC 9292 (Binary HTTP).

+--------------------------------------------------------------------------+
|                 ARCHITECTURE RFC 9458 OBLIVIOUS HTTP (OHTTP)             |
+--------------------------------------------------------------------------+
|                                                                          |
|    +------------+        1. Requête chiffrée (HPKE)       +------------+ |
|    |            | --------------------------------------> |   Serveur  | |
|    |   Client   | <-------------------------------------- |  de relais | |
|    | (Navigateur|        4. Réponse chiffrée (HPKE)       |  (Relay)   | |
|    |  Agent IA) |                                         +------------+ |
|    +------------+                                               |        |
|          |                                            2. Relais |3. Envoi|
|          | (L'adresse IP est vue UNIQUEMENT du Relay)   du bloc |réponse |
|          | (Le contenu est chiffré pour le Target)      chiffré |chiffrée
|          |                                                      v        |
|          |                                                +------------+ |
|          +----------------------------------------------> |   Serveur  | |
|                 (Frontière cryptographique de bout en bout)   cible    | |
|                 Le serveur cible déchiffre via HPKE       |  (Target)  | |
|                 Le serveur cible ne voit QUE L'IP DU RELAY+------------+ |
|                                                                          |
+--------------------------------------------------------------------------+

Fondements cryptographiques : HPKE et format Binary HTTP

La norme RFC 9458 repose sur une séparation rigoureuse des rôles entre deux entités indépendantes : le Relais (Relay) et le Serveur cible (Target). Le Relais achemine des blocs de données chiffrées sans disposer de la clé pour les ouvrir ; le Serveur cible traite les requêtes applicatives sans jamais connaître l'adresse réseau d'origine.

Cette garantie s'articule autour de deux briques normatives majeures :

1. Cadrage binaire des messages HTTP (RFC 9292)

Les requêtes HTTP traditionnelles utilisent des en-têtes textuels associés à un corps de message. Avec OHTTP, la totalité de la transaction — méthode, chemin d'accès, en-têtes et charge utile — est sérialisée en un flux binaire compact selon la norme RFC 9292 (BHTTP) :

+--------------------+--------------------------------+--------------------+
| Données de cadrage | En-têtes indexés               | Corps applicatif   |
| (GET /api/v1/query)| (Host: target.example.com ...) | (Données binaires) |
+--------------------+--------------------------------+--------------------+

Ce format binaire élimine toute ambiguïté syntaxique avant l'étape de scellement cryptographique.

2. Chiffrement hybride à clé publique (HPKE, RFC 9180)

Afin d'interdire au Relais toute lecture indiscrète, le client chiffre le message Binary HTTP au moyen de la configuration de clé publique du Serveur cible. Le serveur Target publie une configuration OHTTP contenant :

  • key_id : identifiant sur 1 octet pour la rotation des clés ;
  • kem_id : mécanisme d'encapsulation de clé (0x0020 pour DHKEM sur Curve25519) ;
  • kdf_id : fonction de dérivation de clé (0x0001 pour HKDF-SHA256) ;
  • aead_id : algorithme de chiffrement authentifié (0x0001 pour AES-128-GCM ou 0x0003 pour ChaCha20-Poly1305) ;
  • public_key : la clé publique brute du Serveur cible.

En appliquant le mode de base de HPKE, le client engendre une paire éphémère $(sk_E, pk_E)$ et dérive un secret partagé avec la clé publique $pk_T$ du Serveur cible :

$$ ext{SharedSecret} = ext{DH}(sk_E, pk_T)$$

À l'aide de la fonction HKDF, le client génère la clé de chiffrement AEAD $K_{ ext{req}}$ et le vecteur d'initialisation $N_{ ext{req}}$ :

$$(K_{ ext{req}}, N_{ ext{req}}) = ext{KeyDerivation}( ext{SharedSecret}, ext{info})$$

La requête Binary HTTP est ensuite scellée dans le texte chiffré $C_{ ext{req}}$ :

$$C_{ ext{req}} = ext{Seal}(K_{ ext{req}}, N_{ ext{req}}, ext{aad}, ext{BinaryHTTPRequest})$$

Le message envoyé au Relais adopte le type MIME message/ohttp-req, structuré ainsi : [KeyID (1 o)] || [KEM (2 o)] || [KDF (2 o)] || [AEAD (2 o)] || [enc (32 o)] || [Texte chiffré]

Seul le Serveur cible détenant la clé privée associée à $pk_T$, le Relais ne peut ni examiner ni altérer les données sans provoquer un échec de vérification AEAD.

+--------------------------------------------------------------------------+
|                  RÉPARTITION DE LA VISIBILITÉ DES DONNÉES                |
+--------------------------------------------------------------------------+
| Acteur      | Connaît l'IP réelle ? | Connaît le contenu ? | Lit la réponse ?|
+-------------+-----------------------+----------------------+-----------------+
| Opérateur   | Oui                   | NON (Tunnel TLS)     | NON             |
| Relais OHTTP| Oui (ou IP VPN)       | NON (Chiffré HPKE)   | NON             |
| Cible Target| NON (Voit IP Relais)  | OUI (BHTTP décodé)   | OUI             |
+--------------------------------------------------------------------------+

Déroulement d'une transaction Oblivious HTTP

Le cycle complet de communication s'organise en cinq phases ordonnées :

  1. Récupération des clés : Le client obtient la configuration de clé publique OHTTP du Serveur cible par l'intermédiaire d'enregistrements DNS HTTPS ou d'annuaires sécurisés.
  2. Encapsulation locale : Le client sérialise la requête au format BHTTP, initialise le contexte HPKE éphémère, scelle les données et produit le bloc message/ohttp-req.
  3. Acheminement par le Relais : Le client transmet le bloc au Relais par une requête HTTPS POST /relay. Le Relais termine la session TLS du client, retire les en-têtes réseau sources et réémet la charge chiffrée vers le Serveur cible.
  4. Déchiffrement et exécution : Le Serveur cible identifie le key_id, désencapsule enc pour reconstituer le secret partagé, déchiffre la charge utile et traite la requête applicative (par exemple l'inférence d'un modèle d'IA).
  5. Scellement de la réponse : Le Serveur cible dérive une clé de réponse $K_{ ext{res}}$ depuis le contexte HPKE, chiffre les données en un bloc message/ohttp-res et le transmet au Relais. Ce dernier le retourne au client pour déchiffrement final.

Complémentarité avec RiceGuard et les tunnels Layer 3

Bien qu'OHTTP opère au niveau applicatif (L7), son association avec une couche de transport Layer 3 (telle que l'architecture RiceGuard de VPNRice) garantit une isolation multicouche :

  1. Neutralisation du profilage au niveau du Relais : Lors de l'acheminement sous RiceGuard, le Relais observe une adresse IP issue de passerelles VPN mutualisées au lieu d'une adresse de connexion résidentielle. Même en cas d'audit malveillant du Relais, aucun lien ne peut être établi avec un abonné précis.
  2. Étanchéité DNS absolue : La résolution des noms de domaine du Relais et de la cible s'exécute strictement au sein de l'espace réseau isolé de RiceGuard, empêchant toute corrélation externe sur l'ouverture de sessions OHTTP.
  3. Sécurisation des agents IA autonomes : Les agents logiciels exécutant des inférences locales ou des appels d'outils externes peuvent canaliser leurs sorties via OHTTP sur RiceGuard, garantissant aux utilisateurs que les éditeurs de modèles ne collectent pas d'empreintes IP pérennes.

Implémentation logicielle : client OHTTP en Go

Voici un exemple d'implémentation en langage Go montrant comment un client prépare et encapsule une requête Binary HTTP avant de l'expédier au Relais :

package main

import (
	"bytes"
	"crypto/rand"
	"fmt"
	"io"
	"net/http"

	"github.com/cloudflare/circl/hpke"
)

type OHTTPEncapsulator struct {
	suite    hpke.Suite
	targetPK hpke.PublicKey
	keyID    uint8
}

func NewOHTTPEncapsulator(keyID uint8, kemID hpke.KEM, kdfID hpke.KDF, aeadID hpke.AEAD, rawPubKey []byte) (*OHTTPEncapsulator, error) {
	suite := hpke.NewSuite(kemID, kdfID, aeadID)
	pk, err := suite.KEM().Scheme().UnmarshalBinaryPublicKey(rawPubKey)
	if err != nil {
		return nil, fmt.Errorf("impossible de lire la clé publique : %w", err)
	}
	return &OHTTPEncapsulator{
		suite:    suite,
		targetPK: pk,
		keyID:    keyID,
	}, nil
}

// EncapsulateRequest transforme un message Binary HTTP en paquet message/ohttp-req
func (oe *OHTTPEncapsulator) EncapsulateRequest(bhttpReq []byte) ([]byte, hpke.Context, error) {
	sender, err := oe.suite.NewSender(oe.targetPK, []byte("message/bhttp request"))
	if err != nil {
		return nil, nil, fmt.Errorf("échec création émetteur HPKE : %w", err)
	}

	enc, ctx, err := sender.Setup(rand.Reader)
	if err != nil {
		return nil, nil, fmt.Errorf("échec initialisation HPKE : %w", err)
	}

	ciphertext, err := ctx.Seal(nil, bhttpReq)
	if err != nil {
		return nil, nil, fmt.Errorf("échec scellement cryptographique : %w", err)
	}

	var buf bytes.Buffer
	buf.WriteByte(oe.keyID)
	buf.Write([]byte{0x00, 0x20}) // DHKEM(X25519, HKDF-SHA256)
	buf.Write([]byte{0x00, 0x01}) // HKDF-SHA256
	buf.Write([]byte{0x00, 0x01}) // AES-128-GCM
	buf.Write(enc)
	buf.Write(ciphertext)

	return buf.Bytes(), ctx, nil
}

func SendToRelay(relayURL string, targetURL string, ohttpPayload []byte) ([]byte, error) {
	req, err := http.NewRequest("POST", relayURL, bytes.NewReader(ohttpPayload))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "message/ohttp-req")
	req.Header.Set("Target", targetURL)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("échec transmission au relais : %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("le relais a rejeté la requête : HTTP %d", resp.StatusCode)
	}

	return io.ReadAll(resp.Body)
}

Arbitrages d'ingénierie et perspectives

Le déploiement de la RFC 9458 implique plusieurs considérations pratiques :

  • Impact sur la latence : Le transit par deux serveurs distincts (Client $ o$ Relais $ o$ Cible) allonge le temps de réponse d'environ 20 à 45 ms. Ce protocole convient idéalement aux appels d'API, à la télémétrie et aux inférences d'IA discrètes plutôt qu'au chargement de sites web interactifs complexes.
  • Gestion des caches : Les mécanismes de mise en cache CDN s'appuient d'ordinaire sur la lecture des URL. Comme les flux OHTTP sont opaques, la mise en mémoire tampon intermédiaire nécessite des balises de cache dédiées.
  • Séparation institutionnelle : La fiabilité du système impose que le Relais et la Cible soient opérés par des organisations indépendantes afin de garantir l'absence de collusion de journaux.

La norme RFC 9458 Oblivious HTTP démontre que la protection des métadonnées réseau peut être résolue rigoureusement sans compromettre la structure des protocoles modernes.