Windows Server 2016/2019 : LUID, conformité NIST SP 800‑90A et vraie RNG avec BCryptGenRandom

Vous devez prouver à un client que les LUID de Windows Server offrent un « hasard » conforme NIST SP 800‑90A ? Mauvaise piste : ils sont séquentiels. Voici les preuves, les implications sécurité et la bonne solution : utiliser le DRBG CNG (via BCryptGenRandom) et, si requis, l’exécution en mode FIPS.

Sommaire

Vue d’ensemble de la question

Dans Windows Server 2016/2019, chaque ouverture de session se voit attribuer un Locally Unique Identifier (LUID) de 64 bits. Empiriquement, on observe que :

  • Les 32 bits de poids fort restent à 0 pendant très longtemps.
  • Les 32 bits de poids faible varient à chaque connexion et « semblent » aléatoires à l’œil nu.
  • La documentation publique ne revendique aucune propriété de hasard cryptographique pour les LUID.

Le demandeur souhaite démontrer à un client que la partie dite « aléatoire » (les 32 bits bas) proviendrait d’un RNG conforme à NIST SP 800‑90A. Nous allons montrer pourquoi c’est faux, quelles garanties réelles fournir, et comment satisfaire une exigence de conformité sans ambiguïté.

Rappel : qu’est‑ce qu’un LUID ?

Un LUID est un entier 64 bits dont l’unique promesse est l’unicité locale (au sein d’une même machine) sur la durée d’exécution du système. Il est utilisé par le noyau et les API de sécurité pour identifier une session de connexion, un privilège activé, ou certains objets de sécurité. En d’autres termes : le LUID est un identifiant, pas un jeton cryptographique.

Conséquences immédiates :

  • Le LUID n’a pas à être imprévisible ; il a juste à ne pas collider localement avant redémarrage.
  • Son cycle de vie est lié au système : au reboot, le compteur repart et de nouveaux LUID seront réémis.
  • Unicité « locale » ≠ unicité globale : un LUID d’une machine A peut être identique à un LUID d’une machine B.

Ce que Windows garantit (et ne garantit pas)

Les API Win32 et NT précisent qu’un LUID est locally unique. Elles ne promettent pas que le LUID soit aléatoire, imprévisible, tiré d’un DRBG, ni qu’il satisfasse des tests statistiques. Il ne faut donc pas extrapoler des propriétés qu’elles ne revendiquent pas.

Comment Windows fabrique réellement un LUID

Le comportement observé et les implémentations libres compatibles (par ex. celles inspirées de ReactOS) convergent : la fonction noyau Zw/NtAllocateLocallyUniqueId renvoie la valeur d’un compteur monotone partagé. Le schéma logique est le suivant :

// Pseudocode conceptuel
// Un compteur global 64 bits, initialisé au boot.
GLOBAL volatile uint64_t g_LuidCounter = 0;

LUID AllocateLocallyUniqueId() {
// Opération atomique pour la sécurité en concurrence.
uint64_t v = InterlockedIncrement64(&g_LuidCounter);
// Projection sur la structure LUID (HighPart/LowPart).
LUID id;
id.LowPart  = (uint32_t)(v & 0xFFFFFFFF);
id.HighPart = (int32_t)((v >> 32) & 0xFFFFFFFF);
return id;
} 

Pourquoi voit‑on si souvent les 32 bits hauts à zéro ? Parce que sur la plupart des serveurs, on n’émettra pas 232 LUID avant le prochain redémarrage. Tant que la partie basse ne déborde pas, la partie haute reste à 0. Ce pattern ne traduit pas une « portion aléatoire », mais simplement l’état d’un compteur dont seule la partie basse évolue.

Indices empiriques faciles à vérifier

  • Après un redémarrage, les premiers LUID reprennent à des valeurs basses (selon l’initialisation du compteur).
  • Si l’on capture une série de LUID sur un serveur calme, la différence entre deux LUID successifs est souvent 1.
  • Sur une machine très sollicitée, la progression peut paraître « chaotique », mais reste compatible avec un incrément monotone concurrent.

Pourquoi cela ne répond pas à NIST SP 800‑90A

La publication NIST SP 800‑90A définit trois familles de Deterministic Random Bit Generators (DRBG) : Hash_DRBG, HMAC_DRBG et CTR_DRBG. Un DRBG conforme doit respecter un cycle de vie normé (instantiate, reseed, generate, uninstantiate), passer des auto‑tests, et s’appuyer sur une source d’entropie adéquate (cf. 800‑90B/90C).

Or, l’API d’allocation des LUID ne revendique aucun de ces mécanismes. C’est un compteur, pas un générateur pseudo‑aléatoire validé. Dès lors, il est impossible de soutenir que « les 32 bits de poids faible d’un LUID » sont issus d’un DRBG conforme NIST SP 800‑90A.

Le RNG conforme à utiliser sous Windows

Quand une exigence contractuelle ou réglementaire impose un DRBG conforme, la voie correcte est d’utiliser la pile cryptographique CNG de Windows (Cryptography Next Generation) :

  • BCryptGenRandom avec l’option BCRYPT_USE_SYSTEM_PREFERRED_RNG fournit des octets aléatoires provenant du RNG système.
  • En environnement soumis à FIPS 140‑3, l’activation du mode FIPS s’assure que seules des primitives approuvées sont utilisées et que les opérations passent par des modules validés. Les identifiants exacts de certificats CMVP dépendent de la version/édition et de la plateforme ; vérifiez la référence applicable à votre build.
  • Dans .NET (Core/5+/6+), RandomNumberGenerator.Fill et RandomNumberGenerator.GetBytes s’appuient sur le RNG système.

Exemple C : tirer 4 octets avec le RNG système

#include <windows.h>
#include <bcrypt.h>
#include <stdio.h>
#pragma comment(lib, "bcrypt.lib")

int main(void) {
UINT32 r = 0;
NTSTATUS st = BCryptGenRandom(NULL, (PUCHAR)&r, sizeof(r), BCRYPT_USE_SYSTEM_PREFERRED_RNG);
if (st < 0) { // BCRYPT_SUCCESS(st) équivaut à (st >= 0)
fprintf(stderr, "BCryptGenRandom a échoué (0x%08X)\n", st);
return 1;
}
printf("RNG CNG (32 bits) = 0x%08X\n", r);
return 0;
} 

Exemple PowerShell (.NET) : 16 octets conformes

[byte[]]$buf = New-Object byte[] 16
[System.Security.Cryptography.RandomNumberGenerator]::Fill($buf)
$hex = ($buf | ForEach-Object { $_.ToString("X2") }) -join ""
"RNG CNG (128 bits) = $hex"

Procédure de démonstration : prouver que le LUID est séquentiel

Voici une démarche simple, reproductible, que vous pouvez intégrer à votre dossier d’acceptation :

  1. Redémarrez une machine de test pour repartir d’un compteur propre.
  2. Collectez les LUID des sessions en cours : en PowerShell, la classe CIM Win32_LogonSession expose la propriété LogonId (le LUID). $sessions = Get-CimInstance Win32_LogonSession | Select-Object LogonId $sessions | Sort-Object LogonId | Select-Object -First 10 | ForEach-Object { [UInt64]$id = $_.LogonId $hi = $id -shr 32 $lo = $id -band 0xFFFFFFFF "{0:X8}-{1:X8}" -f $hi, $lo }
  3. Ouvrez plusieurs sessions (RDP, console, tâches planifiées) et répétez l’extraction. Vous observerez que la colonne « partie basse » s’incrémente, alors que la partie haute reste à 0.
  4. Si vous scriptiez l’allocation directe (C/C++ via AllocateLocallyUniqueId), deux appels successifs renvoient des valeurs adjacentes ou quasi adjacentes.

Cette démonstration illustre la nature « compteur ». Elle n’implique aucune propriété de hasard.

Conséquences sécurité et conformité

  • Imprévisibilité : nulle. Un attaquant disposant d’un échantillon peut extrapoler les prochains LUID.
  • Utilisation correcte : indexer des sessions, tracer des événements, corréler des privilèges. L’unicité locale suffit.
  • Utilisation incorrecte : sel, nonce, jeton CSRF, clé de session, identifiant opaque exposé au client, tirage de loterie, etc.
  • Conformité NIST SP 800‑90A : non applicable aux LUID. Si la conformité est exigée, utilisez le DRBG CNG (ou un module matériel validé) et documentez‑le.

Tableau récapitulatif : besoin vs. recommandation

Besoin du clientRecommandation
Identifier de façon unique une sessionLe LUID natif suffit : unicité locale garantie jusqu’au prochain redémarrage.
Preuve de conformité NIST SP 800‑90ANe pas s’appuyer sur le LUID. Utiliser BCryptGenRandom ou un module validé (HSM, CSP/CNG FIPS), et produire la référence CMVP de la plateforme.
Audit / contratDécrire le LUID comme « identifiant séquentiel interne ». Joindre l’attestation FIPS applicable (p. ex. certificats 140‑3 selon plateforme) pour montrer qu’un DRBG conforme existe, mais distinct de l’allocation des LUID.

Comparatif technique : LUID vs RNG CNG

CaractéristiqueLUIDRNG CNG (BCryptGenRandom)
ObjectifUnicité locale d’objets/sessionsProduction d’octets aléatoires
ImprévisibilitéNonOui (DRBG)
Conformité NIST SP 800‑90ANon applicable / non revendiquéOui (via DRBG validé par la pile CNG/CMVP)
Comportement observéCompteur monotone, HighPart souvent 0Distribution uniforme (tests 800‑22/auto‑tests)
Cas d’usageIndex, corrélation, traceClés, nonces, tokens, identifiants opaques

FAQ

Activer le mode FIPS rend‑il les LUID « conformes » ?

Non. Le mode FIPS contraint la cryptographie système (algorithmes approuvés, modules validés). L’allocation des LUID reste un mécanisme noyau indépendant et séquentiel.

Les LUID peuvent‑ils entrer en collision ?

Par conception, non sur une même machine entre deux redémarrages : l’allocation est atomique et monotone. Après redémarrage, de nouvelles valeurs peuvent réapparaître, ce qui est acceptable puisque l’unicité promise est locale et temporelle.

Quelle différence entre LUID et GUID (UUID) ?

Les GUID/UUID (ex. CoCreateGuid ou Guid.NewGuid()) visent l’unicité globale (128 bits) et non la conformité cryptographique. Selon la version/implémentation, ils peuvent être aléatoires, pseudo‑aléatoires ou structurés. Ne les utilisez pas comme source aléatoire pour des secrets.

Que dire au client qui exige « un identifiant aléatoire » ?

Proposez un identifiant opaque dérivé d’un RNG conforme (CNG) et éventuellement encodé en Base64/hex, distinct du LUID interne. Conservez le LUID pour la corrélation côté serveur.

Bonnes pratiques d’ingénierie

  • Ne jamais dériver des secrets à partir du LUID (même haché) : un hachage d’entrée prédictible reste prédictible pour un attaquant.
  • Pour tout « sel/nonce » : tirez‑le via BCryptGenRandom / RandomNumberGenerator.
  • Enregistrer le LUID dans les journaux d’audit pour la traçabilité locale et la corrélation d’événements.
  • Si vous exposez un identifiant à des clients externes, utilisez un jeton opaque généré par RNG, non dérivable du LUID.

Exemples concrets d’intégration

Émission d’un jeton d’API côté serveur

  1. Générez 16–32 octets via BCryptGenRandom.
  2. Encodez en Base64URL.
  3. Enregistrez en base le couple : (LUID interne, jeton externe, horodatage).
  4. Exposez uniquement le jeton externe aux clients.

Journalisation d’accès RDP

Ajoutez aux logs : LUID, SID utilisateur, Poste, Adresse IP, Jeton aléatoire. Le LUID facilite la corrélation locale ; le jeton aléatoire répond aux politiques qui exigent des identifiants non devinables.

Encadré : NIST SP 800‑90A, 90B, 90C en 60 secondes

  • SP 800‑90A : définit les DRBG (Hash/HMAC/CTR) et leur cycle de vie.
  • SP 800‑90B : évalue la source d’entropie.
  • SP 800‑90C : assemble source d’entropie + DRBG en un RNG robuste.

Un identifiant séquentiel (LUID) n’entre dans aucune de ces catégories ; un DRBG validé (CNG) oui.

Annexe : activer le mode FIPS (si requis par politique)

Chemin GPO : Computer Configuration / Windows Settings / Security Settings / Local Policies / Security Options / System cryptography: Use FIPS compliant algorithms.

Prudence : le mode FIPS peut désactiver certaines API non approuvées ; validez vos applications. Le RNG CNG reste accessible (BCryptGenRandom) et fournit des octets conformes.

Checklist d’audit à joindre au dossier

  • Constat LUID : série capturée montrant HighPart = 0 et LowPart monotone.
  • Justification : le LUID est un compteur noyau, sans revendication d’aléa.
  • Solution : RNG CNG via BCryptGenRandom (ou RNG matériel) pour tout besoin d’aléa/nonce/clé.
  • Conformité : référence au statut FIPS/CMVP applicable à la plateforme, environnement de test et version.
  • Preuve : code ou scripts utilisés, empreintes binaires et captures datées.

Modèle de clause contractuelle

Pour les identifiants internes (LUID) utilisés par le système, l’unicité locale est garantie par le mécanisme d’allocation de Windows. Pour toute exigence d’aléa cryptographique et de conformité NIST SP 800‑90A, l’application s’appuie exclusivement sur le générateur d’octets pseudo‑aléatoires fourni par la pile CNG (BCryptGenRandom) et/ou sur des modules cryptographiques validés (FIPS 140‑3). Les LUID ne sont ni utilisés ni présentés comme sources d’aléa ou secrets.

Annexe : script PowerShell de vérification rapide

Le script ci‑dessous imprime les 30 plus petits LUID actifs, séparés en partie haute/basse. Vous devriez voir la partie haute rester à 0 tant que la machine n’a pas émis plus de 4 294 967 296 identifiants depuis le boot.

$list = Get-CimInstance Win32_LogonSession | Sort-Object LogonId | Select-Object -First 30
"{0,12} {1,10} {2,10}" -f "LogonId","High32","Low32"
$list | ForEach-Object {
  [UInt64]$id = $_.LogonId
  $hi = $id -shr 32
  $lo = $id -band 0xFFFFFFFF
  "{0,12} {1,10:X8} {2,10:X8}" -f $id, $hi, $lo
}

Ce qu’il faut retenir

  • Le LUID est un compteur de 64 bits à usage interne, garantissant l’unicité locale, pas un RNG.
  • Il n’existe aucune base pour affirmer une conformité NIST SP 800‑90A au niveau des LUID.
  • Pour toute exigence d’aléa et de conformité : utilisez BCryptGenRandom / RandomNumberGenerator (pile CNG), et joignez la référence FIPS/CMVP appropriée.
  • Documentez clairement la séparation : LUID pour la corrélation interne, jetons aléatoires pour les usages externes et sensibles.

Questions avancées (pour les relecteurs sécurité)

Et si nous « mélangions » le LUID avec du hasard (ex. HMAC(LUID, RNG)) ?

C’est acceptable si et seulement si la clé HMAC provient d’une source aléatoire sûre et que le LUID n’est qu’une donnée publique. Dans ce cas, la sécurité vient exclusivement du secret/HMAC, pas du LUID. Le LUID n’ajoute ni n’enlève d’entropie au calcul.

Peut‑on transformer un compteur en RNG conforme via un extracteur ?

Non. Sans entropie initiale, un extracteur (hash, HMAC, KDF) ne crée pas de hasard. Il « lisse » au mieux une entropie existante. Il faut tirer l’aléa à la source (CNG/DRBG), puis éventuellement combiner avec un compteur pour obtenir des identifiants monotones mais non devinables.

Performance : le RNG CNG sera‑t‑il un goulot d’étranglement ?

Dans la plupart des applications, non. BCryptGenRandom est optimisé et thread‑safe. Si vous avez des besoins à très haut débit, allouez en blocs (ex. 4–16 Ko par appel) et redistribuez en mémoire.

Conclusion

Sur Windows Server 2016/2019, la « variabilité » observée des LUID vient d’un simple compteur noyau. Elle ne constitue ni de l’aléa ni une conformité NIST SP 800‑90A. Pour répondre à des exigences réglementaires ou clients, déclarez explicitement la fonction du LUID (unicité locale) et basez tout besoin d’aléa sur la pile CNG (BCryptGenRandom) ou un module validé. Cette séparation des responsabilités est à la fois précise, auditable et robuste.

Sommaire