Saltar al contenido principal
fr/blog/obscura/research/canvas-webgl-fingerprinting/

obscura/research/canvas-webgl-fingerprinting

4 min de lecture

Aperçu

L'empreinte Canvas exploite les différences subtiles dans la façon dont le matériel et le logiciel rendent les graphiques. Ces différences proviennent de :

  • Modèle GPU et version du pilote
  • Rendu des polices du système d'exploitation (anti-aliasing, hinting, rendu subpixel)
  • Moteur de rendu du navigateur
  • Polices système installées

Empreinte Canvas

Mécanisme

  1. Le site dessine des formes et du texte sur un élément <canvas> HTML5
  2. Appelle canvas.toDataURL() pour extraire les données de pixels en base64
  3. Hache le résultat (MD5, SHA-1, ou hachage personnalisé)
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.font = '14px Arial'
ctx.fillStyle = '#f60'
ctx.fillRect(125, 1, 62, 20)
ctx.fillStyle = '#069'
ctx.fillText('BrowserLeaks,com <canvas> 1.0', 2, 15)

const fingerprint = canvas.toDataURL()
const hash = md5(fingerprint) // unique par périphérique

Sources d'Entropie

Source Variabilité
Rendu des polices (anti-aliasing, hinting) Élevée
Ordre de rendu subpixel (RVB vs BVR) Moyenne
Rastérisation GPU des formes Moyenne
Conversion d'espace colorimétrique Faible

Empreinte WebGL

Mécanisme

WebGL expose le GPU directement via :

const gl = canvas.getContext('webgl')
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info')
const renderer = debugInfo.UNMASKED_RENDERER_WEBGL
// ex., "NVIDIA GeForce RTX 3080/PCIe/SSE2"
const vendor = debugInfo.UNMASKED_VENDOR_WEBGL
// ex., "NVIDIA Corporation"

De plus, le rendu de scènes 3D produit une sortie de pixels spécifique au matériel.

Signaux

  • UNMASKED_RENDERER_WEBGL — modèle GPU exact
  • UNMASKED_VENDOR_WEBGL — fabricant GPU
  • Précision des shaders WebGL
  • MAX_TEXTURE_SIZE, MAX_VERTEX_ATTRIBS, etc.
  • Rendu de scène 3D (hachage des pixels)

Empreinte WebGPU

WebGPU (le successeur de WebGL) expose des informations encore plus granulaires :

const adapter = await navigator.gpu.requestAdapter()
const info = await adapter.requestAdapterInfo()
// { vendor: "nvidia", architecture: "ampere", device: "..." }

Signaux

  • adapterInfo.vendor
  • adapterInfo.architecture
  • adapterInfo.device
  • Ensemble de fonctionnalités (quelles fonctionnalités GPU sont supportées)

Pourquoi l'Usurpation au Niveau Proxy est Difficile

Le rendu Canvas/WebGL/WebGPU s'effectue sur le GPU local. Le proxy ne voit que le trafic réseau. Au moment où les données atteignent le proxy, le rendu est déjà terminé.

Approches d'atténuation possibles :


  1. Bloquer l'API : Faire en sorte que WebGL retourne null, canvas génère une erreur. Problème : détectable comme signal ("WebGL bloqué" est lui-même un trait d'empreinte).


  2. Injecter du bruit JS : CanvasBlocker (Firefox) randomise la sortie canvas en injectant un pixel aléatoire avant toDataURL(). Problème : si le site lit le canvas 3 fois et obtient 3 hachages différents, il détecte la manipulation. Nécessite également un bruit cohérent en fréquence.

  3. Surcharge ImageData via Proxy : Surcharger CanvasRenderingContext2D.prototype.getImageData via JS injecté pour retourner des données génériques. Problème : fonctionne seulement sur getImageData, pas toDataURL. Chemins multiples pour extraire les pixels.


  4. Rendu GPU logiciel (SwiftShader/Lavapipe) : Remplacer le rendu GPU par un rendu déterministe basé sur CPU. Problème : coût de performance massif, pas d'accélération 3D.


Formules

Entropie de l'Empreinte Canvas

Hcanvas=i=1nP(xi)log2P(xi)H_{canvas} = -\sum_{i=1}^{n} P(x_i) \log_2 P(x_i)

P(xi)P(x_i) est la probabilité d'un hachage donné sur différents appareils.

Entropie estimée : ~5-7 bits (peut distinguer ~32-128 appareils).

Probabilité d'Unicité

P(unique)=1i=1kNiNP(\text{unique}) = 1 - \prod_{i=1}^{k} \frac{N - i}{N}

NN est la taille de la population et kk est le nombre de sorties canvas distinctes dans l'échantillon.

Conclusions

  • L'empreinte Canvas/WebGL ne peut pas être entièrement usurpée au niveau proxy
  • L'injection de bruit via JS est une atténuation partielle mais détectable
  • Bloquer ces API est détectable et devient un signal en soi
  • La seule solution quasi-complète est l'approche de Tor Browser (normaliser tous les utilisateurs vers une sortie identique)
  • Pour Obscura : la meilleure approche est du JS injecté qui ajoute un bruit cohérent + bloque WEBGL_debug_renderer_info