Utilisation de math/rand avec nanoseconde courante vs crypto/rand en Go

Dans le cadre de la génération de secrets tels que des jetons de session ou des mots de passe, les concepts de base de la sécurité nous imposent un certain aléa qui empêchent un attaquant de deviner le secret, même s’il connait ses paramètres de génération.

Bien sûr, nous allons plutôt utiliser « crypto/rand » parce qu’il est étiqueté « crypto », ou parce qu’il est recommandé dans la charte du développement sécurisé du RSSI.

L'entropie

Pour mesurer la solidité d’un secret, on utilise l’entropie. Très grossièrement, l’entropie mesure l’aléa réel ou le désordre. Plus elle est élevée, plus le désordre est grand. Pour calculer l’entropie d’un mot de passe, on prend toutes les combinaisons possibles de caractères réalisables pour une taille de mot de passe donnée. Cela va donner un nombre de combinaisons. On exprime ensuite ce nombre de combinaisons en bit. L’entropie est exprimée en bit car un bit est la plus petite unité de donnée mesurable.

Pour convertir un nombre de possibilités en bit, il faut résoudre l’équation 2x = y ou « y » est le nombre de possibilités et « x » le nombre de bits. La réponse est donc trouvée avec un logarithme en base 2 (log2), soit log2(y) = x.

Donc, un mot de passe d’une longueur de 2 éléments ou chaque élément est un chiffre pourra prendre 100 valeurs différentes, de « 00 » à « 99 », soit une entropie de 6bits.

Si la politique de mot de passe permet les caractères alphabétiques, l’utilisateur pourra utiliser des noms communs ou des noms propres, tels que « anne », « voiture », … Cet usage fait considérablement baisser l’entropie. En effet, les attaques par dictionnaires supposent que la plupart des utilisateurs vont utiliser des noms communs, et vont précalculer des mots de passe utilisant ces noms communs en priorité. Par conséquent, cet usage réduit le nombre de combinaisons possibles et donc l'entropie

Fin 2022, les valeurs recommandées par la CNIL (https://www.cnil.fr/fr/mots-de-passe-une-nouvelle-recommandation-pour-maitriser-sa-securite) pour générer un mot de passe sont :

  • Mot de passe simple, sans contrôle complémentaire : 80bits
  • Mot de passe avec contrôles (nombres de tentative échouées, …) : 50bits

Les générateurs pseudo aléatoires

« math/rand » n’est pas un bon candidat pour générer des secrets car c’est un générateur pseudo-aléatoire. Cela signifie qu’il génère une séquence de valeurs qui semble aléatoire mais qui ne l’est absolument pas. En effet, si on connait la graine et l’algorithme on peut prédire la séquence. Il est donc déterministe et prévisible. La graine fournie à « math/rand » est un entier signé de 64bit. Si cet entier est aléatoire, l’entropie de tout ce qui sera généré par « math/rand » sera de 64bits. Il est très facile de s’assurer de ce comportement avec le code ci-dessous. Il s’agira de faire varier la graine et de comparer les résultats.

package main

import "math/rand"
import "fmt"

func main() {
   var rng *rand.Rand

   rng = rand.New(rand.NewSource(23))
   fmt.Printf("%d %d %d %d\n", rng.Int(), rng.Int(), rng.Int(), rng.Int())

   rng = rand.New(rand.NewSource(23))
   fmt.Printf("%d %d %d %d\n", rng.Int(), rng.Int(), rng.Int(), rng.Int())
}

En guise de graine, on pourrait penser que la date courante en nanosecondes est suffisamment aléatoire pour générer un secret. En effet, il y a beaucoup de nanosecondes dans une seconde et l’imprécision de l’horloge du système ajoute un aléa. On peut penser qu’il sera très difficile pour l’attaquant de deviner la date de génération d’un secret, surtout en nanosecondes. L’attaquant peut encadrer la date de génération dans le temps afin de réduire les possibilités, mais un mot de passe généré aléatoirement sera difficile à encadrer dans le temps. Retenons un encadrement raisonnable d’un mois, en effet l’attaquant pourra estimer à quel mois un utilisateur a souscrit un service. Soyons honnête, je retiens la valeur d’un mois parce que ça m’arrange pour la suite :-). Dans la pratique, l’attaquant pourra être plus précis.

On a donc une entropie de log2(10^9 * 3600 * 24 * 30) = 51bits. En théorie, cela est compatible avec les règles de la CNIL.

Utiliser des nanosecondes

En Go, on peut utiliser la fonction « time.UnixNano » pour avoir des nanosecondes. Ce serait bien de s’assurer de l’entropie de l’horloge fournie en Go. Vu que le système d’heure fonctionne très bien, on n’aura pas de doutes sur la précision de la seconde. Regardons donc ce qu’il est en des nanosecondes.

package main

import "time"
import "fmt"

func main() {
	var i int

	for i = 0; i < 10000; i++ {
		fmt.Printf("%09d\n", time.Now().Nanosecond())
	}
}

Sur mon Mac « Somona » avec « 2,3 GHz Quad-Core Intel Core i7 », le résultat ressemble à ça :

...
504629000
504630000
504631000
504633000
504634000
504635000
504636000
504638000
504639000
504640000
...

On voit clairement qu’il s’agit de microsecondes et non de nanosecondes. L’entropie est divisée par 1000 pour atteindre : log2(109 x 3600 x 24 x 30 ÷ 1000) = 41bits. Ce n’est pas suffisant.

Sur un serveur « Debian 4.9 » avec « Intel(R) Atom(TM) CPU C2338 @ 1.74GHz ». Le résultat est le suivant :

...
570532701
570544820
570560828
570574580
570586520
570598771
570605839
570640806
570668058
570682145
...

Donc, la réalité de l’entropie basée sur la date va dépendre de la capacité d’un attaquant à encadrer la date de génération du secret et de l’OS/matériel sur lequel on travaille.

Que fait « crypto/rand »

« crypto/rand » est un véritable générateur de nombre aléatoire défini pour un aléa compatible avec de la cryptographie (CSPRNG). Pour assurer l’aléa, il utilise des sources d’entropie fournies par le système d'exploitation pour générer des nombres vraiment aléatoires et imprévisibles. On ne peut donc pas déterminer à l’avance la valeur qu’il va générer. « crypro/rand » est donc préféré pour un usage cryptographique, car il améliorera la securité d’une appliqucation qui l’utilise pour générer ses secrets. En revanche, il est plus couteux en temps que « math/rand ».

« math/rand » sera utilisé pour des situation ou du pseudo aléatoire est suffisant comme la génération des données de tests ou des simulations. La propriété pseudo-aléatoire basée sur une graine peut être intéressante pour de la génération procédurale d’environnements complexes comme les mondes de Minecraft, ou la même graine génère le même monde.

Recommandation

Plutôt que d’émettre une supposition sur la capacité d’un attaquant à deviner une date et émettre une spécification sur le matériel à utiliser pour que le logiciel fonctionne, il est plus simple d’utiliser une source d’aléa non prévisible, ce qui est fourni par « crypto/rand ».