Authentification dans les ONC/RPC
icone programmation
Signature :
Misc
Sommaire de l'article :

Retrouvez cet article dans : Misc 17

Le présent article décrit et analyse les mécanismes d'authentification utilisés dans le protocole ONC/RPC (Open Network Computing Remote Procedure Call). Le périmètre est ensuite élargi sur le protocole RPCSEC_GSS.

1 Avant de commencer

Le but de cet article est de traiter des différentes façons de gérer l'authentification d'un client et d'un serveur basés sur le paradigme ONC/RPC (RFC1832). Nous verrons les différents mécanismes utilisables et leurs limitations avant de regarder de plus près RPCSEC_GSS (RFC2203) qui est une évolution naturelle de ONC/RPC en matière de sécurité grâce à l'introduction du formalisme de la GSS-API. Nous supposerons que le lecteur est déjà familier avec le formalisme des ONC/RPC et qu'il a connaissance des grands principes de la GSS-API (Global Security Service Application Programming Interface, utilisée dans la dernière partie). Quelques rappels liminaires sont fournis pour introduire les notions, mais il ne s'agit en aucun cas d'une référence. Les références situées à la fin de l'article vous permettront de disposer des documents nécessaires sur ces sujets.

1.1 Le protocole ONC/RPC

ONC/RPC a principalement été créé dans le but de fournir une couche de base au protocole NFS. Actuellement, il sert de base à de nombreux protocoles du monde Unix, NFS bien sûr, mais aussi NIS, NIS+ ou RPCBIND. Un client/serveur ONC/RPC suppose que le serveur est accessible pour différentes fonctions, identifiées par un triplet (numéro de programme, numéro de version, numéro de fonction). Par exemple, la fonction READDIR du protocole NFSv3 est identifiée par le triplet (100003, 3, 16). L'utilisation d'une fonction est générique : le client renseigne une structure décrivant les arguments de la fonction, l'envoie au serveur qui effectue le travail et retourne une structure décrivant le résultat de l'opération. Dans la suite de l'article, nous appellerons " requête " le message envoyé par le client sur le serveur et qui contient les arguments, la " réponse " sera le message retourné par le serveur sur le client et contenant le résultat. Un message RPC est composé de deux parties : un en-tête et un corps de message. La structure de l'en-tête est bien connue et décrite dans les spécifications de ONC/RPC, le corps du message dépend du protocole qui utilise RPC pour gérer ces messages. Pour garantir la portabilité des messages entre différentes architectures (BigEndian et LittleEndian) un format de représentation externe de données a été choisi pour ONC/RPC : le format XDR. Le corps du message est encodé/décodé depuis le format propre à l'architecture vers le format XDR.

2. Les différents types d'authentification disponibles dans ONC/RPC

2.1 En-tête d'un appel RPC

L'en-tête du message est tout particulièrement intéressant. Il contient l'identifiant de la fonction appelée ainsi que des informations relatives à l'émetteur du message. Dans le cadre de la gestion de l'authentification, il est très intéressant car c'est ici que l'on trouve les credentials du message. Dans tout ce qui suit, le terme " credentials " désignera une structure de données opaque utilisée pour identifier un acteur au sein d'un mécanisme d'authentification. L'en-tête RPC est défini dans le fichier /usr/include/rpc/rpc_msg.h. Dans la suite de l'article, nous étudierons des traces relevées avec l'utilitaire Snoop. Cet utilitaire se trouve en standard sur Solaris, mais malheureusement pas sous Linux. Cet outil génère des traces extrêmement lisibles, surtout en ce qui concerne les paquets RPC. Nous retrouverons ces structures et ces champs dans les traces de façon non équivoque.
rm_xid est l'identifiant de la requête. Il est principalement utilisé par le client pour s'assurer que le serveur ne lui retourne pas un message destiné à un autre client, ainsi que pour la gestion des requêtes dupliquées et/ou réémises (par exemple un trafic NFS important avec UDP comme couche de transport engendrera des requêtes dupliquées ; rm_direction qui indique le sens du message (requête ou réponse) ; ru est une union discriminée par le champ rm_direction. Dans le cas d'une requête, ce champ prendra la forme d'une structure call_body, et elle sera une structure reply_body dans le cas d'une réponse.
L'essentiel des informations relatives à l'authentification passe dans la requête, il est donc utile d'en savoir un peu plus sur la structure call_body. Cette dernière (qui est aussi définie dans /usr/include/rpc/rpc_msg.h) comporte plusieurs champs :
cb_rpcvers indique le numéro de version des ONC/RPC, valeur fixée dans les spécifications du protocole. Ce champ contiendra toujours la valeur 2 ;  cb_vers, cb_prog, cb_proc forment le triplet (version, programme, fonction) qui identifie totalement la fonction ; cb_cred est un champ opaque qui contient les informations destinées à l'authentification, à savoir les trois informations suivantes :
  • oa_flavor est le type d'authentification utilisé ;
  • oa_base est le début d'un buffer opaque contenant les informations nécessaires pour l'authentification. C'est une information opaque qui n'a de sens que du point de vue interne au mécanisme sous-jacent. Nous verrons dans chaque cas comment l'utiliser ;
  • oa_length est la longueur du buffer précédent ;
cb_verf est un vérificateur, il est utilisé dans certains mécanismes d'authentification.
Chaque mécanisme d'authentification fonctionne de façon similaire : on lit dans cb_cred::oa_flavor une valeur caractéristique de l'authentification utilisée. Connaissant cela, on vient plaquer le buffer opaque cb_cred::oa_base sur une structure bien connue, caractéristique du mécanisme et gérée par XDR. On obtient ainsi les informations nécessaires à la réalisation de l'authentification. Ces données sont opaques du point de vue du développeur qui manipule les appels ONC/RPC depuis son code, mais explicites du point de vue interne au traitement des appels ONC/RPC. Il est important de noter que le buffer cb_cred::oa_base est de taille limitée au plus à 400 octets (cette valeur est contenue dans la constante MAX_AUTH_BYTES). A l'époque où ONC/RPC a été défini, en 1988, on considérait cette valeur comme excessivement grande et on pensait que rendre le buffer trop grand constituerait un surcoût inutile. Nous verrons que c'est une limitation très forte qui a favorisé l'émergence de RPCSEC_GSS.

2.2 Le plus simple c'est de ne rien faire : authentification AUTH_NONE

Pour commencer, on utilise l'authentification AUTH_NONE, qui, comme son nom le suggère fort justement, ne fait aucune authentification. AUTH_NONE n'utilise pas les credentials, ne les lit pas et ne les écrit pas. Bien que d'un très faible intérêt sur le plan de la protection offerte, elle est utile d'un point de vue didactique, car elle va nous permettre de voir ce que sont les traces sans aucun mécanisme utilisé. Par la suite, quand nous utiliserons des outils plus compliqués, nous verrons les traces devenir elles-mêmes plus complexes.

2.2.1 Trace réseau

Les informations suivantes sont extraites des traces réseaux réalisées entre deux machines sous SunOS, réalisées avec l'utilitaire Snoop. Le message qui correspond est une consultation de page NIS, sans aucune authentification :
RPC:  ----- SUN RPC Header -----
     RPC:
     RPC:  Transaction id =  1082628167                       
-> le champ  rm_xid
     RPC:  Type = 0     (Call)                                
-> le champ rm_direction (ici il s'agit d'une requête)
     RPC:  RPC version =     2                                
-> call_body::cb_rpcvers (possède toujours la valeur 2)
     RPC:  Program = 100004 (NIS), version = 2, procedure = 3 
-> call_body::cb_prog, call_body::cb_vers, call_body::cb_proc
     RPC:  Credentials: Flavor = 0 (None), len = 0 bytes      
-> Les credentials, ici de taille vide, on remarque que cb_cred::oa_flavor = 0 = AUTH_NONE
     RPC:  Verifier   : Flavor = 0 (None), len = 0 bytes      
-> le vérificateur, de taille vide également
     RPC:

2.3 Un peu plus compliqué, mais pas très robuste : authentification AUTH_UNIX

L'authentification de type AUTH_UNIX (aussi appelée AUTH_SYS sur certaines plates-formes pour des raisons de copyright) fournit une identification de l'utilisateur très simple, sous la forme d'un triplet (uid, gid, {liste de groupes secondaires}). Elle estampille une requête avec ce couple dans le corps des credentials RPC. Dans ce cas, la structure authunix_parms est alors plaquée sur les credentials :
struct authunix_parms
{
u_long aup_time ;
char *aup_machname ;
__uid_t aup_uid ;
__gid_t aup_gid ;
u_int aup_len ;
__gid_t *aup_gids ;
} ;
  • aup_time est un horodateur, il est généralement non utilisé ;
  • aup_machname est une chaîne de caractères contenant le nom de la machine cliente, limitée à 255 caractères (soit la valeur de la constante MAX_MACHINE_NAME dans auth_unix.h). Ce champ n'est pas utilisé en général, il est considéré comme préférable d'obtenir l'adresse IP de l'appelant (par svc_getcaller) puis de rechercher le nom de machine correspondant. En effet, il est facile de falsifier ce champ et de prétendre provenir d'une autre machine (simplement en manipulant la structure), alors que tromper svc_getcaller revient à duper l'appel standard C getpeername, ce qui est moins simple (bien que très faisable) ;
  • aup_uid est le user id de la personne qui tourne le client ;
  • aup_gid est le group id de la personne qui tourne le client ;
  • aup_gids est un tableau de group ids listant les groupes secondaires auxquels la personne qui tourne le client appartient. Ce tableau est limité à 16 éléments (c'est la constante NGRPS définie dans le fichier auth_unix.h qui indique cette valeur). Ce nombre peut être plus faible sur d'autres OS. En particulier, certaines versions de SunOS limitaient ce nombre à 8 ou 10.
En résumé : AUTH_UNIX est une façon simple et peu coûteuse pour associer une requête à l'utilisateur qui en est à l'origine. Ce type d'authentification ne prétend pas fournir quoi que ce soit d'autre et est très facilement falsifiable, c'est pourquoi elle est délaissée dans les cas où un minimum de sécurité est requis.

2.3.1 Trace réseau

RPC:  ----- SUN RPC Header -----
          RPC:
          RPC:  Transaction id = 2809524949
          RPC:  Type = 0 (Call)
          RPC:  RPC version = 2
          RPC:  Program = 100003 (NFS), version = 3, procedure = 1
          RPC:  Credentials: Flavor = 1 (Unix), len = 32 bytes 
-> cb_cred::oa_flavor = AUTH_UNIX
          RPC:     Time = 28-May-04 06:55:22                   
-> cb_cred::oa_base::authunix_parms::aup_time
          RPC:     Hostname = sanguku                          
-> cb_cred::oa_base::authunix_parms::aup_machname
          RPC:     Uid = 1234, Gid = 5678                      
-> cb_cred::oa_base::authunix_parms::aup_uid, cb_cred::oa_base::authunix_parms::aup_gid
          RPC:     Groups = 5678                               
-> cb_cred::oa_base::authunix_parms::aup_gids (ici l'utilisateur n'appartient qu'à un seul groupe)
          RPC:  Verifier   : Flavor = 0 (None), len = 0 bytes  
-> pas de vérificateur dans ce type d'authentification
          RPC:

2.4 Authentification avec la méthode de Diffie-Hellman : l'authentification AUTH_DH

La méthode d'authentification de Diffie-Hellman (ou AUTH_DH, aussi notée AUTH_DES) est la première authentification vraiment sérieuse à avoir été mise en place. Ses développeurs, David Goldberg et Gary Taylor voulaient en faire une sous-couche pour un NFS sécurisé. Compte tenu des faiblesses évidentes de AUTH_UNIX, une nouvelle solution devait voir le jour. La solution retenue fut d'avoir recours à des algorithmes de chiffrement pour garantir la confidentialité des credentials.

2.4.1 Petit rappel : clef publique et privée selon l'algorithme Diffie-Hellman

La méthode de Diffie-Hellman repose sur l'utilisation des clefs publiques et privées. On ne rentrera pas dans le détail de la génération de ces clefs (cela fait l'objet d'un autre article de ce dossier). Les clefs utilisées dans AUTH_DH ont une longueur fixe de 192 octets. On considère dans ce qui suit que :
  • chaque acteur peut, à partir de sa clef privée et de la clef publique de l'autre, créer une clef commune (qui est la même pour chaque acteur) ;
  • l'opération qui consiste à casser une telle clef (qui se ramène souvent à résoudre un logarithme discret) est supposée longue au regard des durées des échanges entre les acteurs.
On peut imaginer qu'une personne malveillante, disposant de moyens de calcul élaborés, soit capable de résoudre un logarithme discret. Pour s'en protéger, on ajoutera une étiquette temporelle aux informations envoyées, sous la forme d'une date et d'une durée avant expiration (ce que l'on appellera par la suite une " fenêtre de temps "). Si la durée avant expiration est petite devant la durée estimée pour résoudre un logarithme discret, alors notre attaquant ne pourra calculer que des clefs ayant expiré pendant qu'il était en train de les casser. On note que, dans le cadre de la mise en place d'une authentification de ce type, il est critique de s'assurer de la fiabilité de sa base de temps (par exemple en utilisant le Network Time Protocol).

2.4.2 Les credentials RPC avec AUTH_DH

Un utilisateur est identifié sous la forme d'une chaîne de caractères (de longueur inférieure à 255) au lieu d'un entier repérant son uid. Cette chaîne est appelée network name ou netname. On considère que les netnames sont uniques pour chaque client sur le réseau (lequel réseau peut être Internet en entier), et il est de la responsabilité de l'OS de correctement générer des netnames en conformité avec la façon dont il désigne les utilisateurs. Lors du premier appel, le client engendre une clef de conversation. Ensuite, il crée une structure de 128 octets, définissant une fenêtre de temps, regroupant une date epoch et une durée. Cette structure est encodée avec la clef de conversation. Ensuite, il chiffre la clef de conversation avec la clef commune de l'algorithme de Diffie-Hellman. Le client envoie alors, dans les credentials RPC, au format XDR, l'ensemble suivant : àun flag (ou namekind) indiquant que le nom qui suit est un netname et pas un nickname généré par le serveur (cette notion sera définie plus loin) ;
  • son netname complet ;
  • les 128 octets représentant la fenêtre de temps chiffrée avec la clef de conversation ;
  • la clef de conversation chiffrée avec la clef commune Diffie-Hellman.
Quand le serveur reçoit ce message, connaissant la clef commune au sens de Diffie-Hellman, il peut en déduire la clef de conversation et donc lire la fenêtre de temps. S'il se trouve hors de la fenêtre, l'authentification échoue avec l'erreur AUTH_BADCRED. Si le serveur est bien dans la fenêtre de temps, il procède à une autre vérification : s'il connaît un autre credential pour ce client (issu d'une autre connexion), il vérifie que la fenêtre reçue est bien postérieure à la précédente qu'il connaît déjà. Si tel n'est pas le cas, l'authentification échoue avec l'erreur AUTH_REJECTEDCRED. Si les contrôles sont réussis, le serveur engendre un nickname, sous la forme d'une chaîne arbitraire de 64 bits. Le nickname désigne la session établie avec le client, mais sert aussi d'index pour permettre au serveur de retrouver les informations relatives au client qu'il a pris la peine de sauvegarder. Le serveur retourne sur le client (dans le credential RPC) le nickname ainsi qu'une fenêtre de temps chiffrée avec la clef de conversation. Par la suite, le client n'enverra plus son netname complet pour s'identifier, mais simplement le nickname. Ce dernier sera résolu par le serveur pour retrouver les informations relatives à la session et collectées lors de son établissement. Cela réduit considérablement la surcharge induite par la gestion des netnames. De plus, il n'est plus utile d'y inclure la clef de conversation chiffrée, puisque maintenant le serveur la connaît. Le credentials du client est alors simplifié et prend la forme suivante :
  • flag namekind indiquant qu'on utilise un nickname et plus un netname complet ;
  • le nickname sur 64 bits ;
  • une fenêtre de temps de 128 octets chiffrée avec la clef de conversation.
Comme vous l'avez compris, AUTH_DH accorde beaucoup d'importance à la durée de validité de ses structures. Ainsi un nickname peut expirer au niveau du serveur. Dans ce cas, une erreur AUTH_BADCRED est retournée et le client doit utiliser un credential contenant son netname complet. Le serveur lui accordera alors un autre nickname. L'authentification AUTH_DH ne s'arrête pas là et utilise le vérificateur inclus dans la structure de credential (le champ cb_verf). Il est important de comprendre que les credentials sont inclus dans la requête, pas forcément dans la réponse. Le vérificateur est un moyen de garantir aussi la validité de la réponse et non plus de la requête. Un vérificateur engendré par un client sera une date et une fenêtre de temps, chiffré avec la clef de conversation. Un vérificateur retourné par un serveur sera un horodateur chiffré avec la clef de conversation et le nickname de la session. A chaque message, le vérificateur est comparé avec la date courante pour rejeter des messages périmés, donc potentiellement falsifiés.

2.4.3 Faiblesse de AUTH_DH

Il y a un sujet que nous avons scrupuleusement évité : la méthode utilisée par les acteurs pour obtenir la clef publique de leur interlocuteur. Traditionnellement, les acteurs sont désignés par leurs netnames. Un fichier local ou une table NIS définit les associations entre les netnames et les clefs publiques correspondantes. Cette consultation n'est pas sécurisée puisque l'on ne dispose pas encore des informations indispensables à l'authentification (en l'occurrence les clefs publiques). De plus, la longueur de la clef (192 octets) est considérée, de nos jours, comme une valeur trop faible pour fournir une robustesse suffisante. L'authentification AUTH_DH est donc désuète et on lui préfère AUTH_KERB ou RPCSEC_GSS. Toutefois, ce type d'authentification est très instructif car elle illustre bien certains mécanismes qui sont à la base des protocoles développés ultérieurement, en particulier Kerberos.

2.4.4 Trace réseau

La trace qui suit est obtenue si vous utilisez le service NIS+ de Solaris. Ici, on essaye de joindre le daemon rpc.nisd qui implémente la plupart des fonctions du service NIS+. Le premier appel réalisé par le client sera :
RPC:  ----- SUN RPC Header -----
          RPC:
          RPC:  Transaction id = 1874964582
          RPC:  Type = 0 (Call)
          RPC:  RPC version = 2
          RPC:  Program = 100300 (NIS+), version = 3, procedure = 1
          RPC:  Credentials: Flavor = 3 (DES), len = 48 bytes              
-> cb_cred::oa_flavor = AUTH_DH = AUTH_DES
          RPC:     Name kind = 0 (fullname)                                
-> Appel initial pas de nickname encore négocié
          RPC:     Network name  =   unix.78451@sanguku.mydomain           
-> qualification par le network name complet (car name kind = 0)
          RPC:     Conversation key = 0x85DC96471A3B9964  (DES encrypted)  
-> la clef de conversation chiffrée
          RPC:     Window = 0x459674CD (DES encrypted)                     
-> fenêtre de validité du message
          RPC:  Verifier       : Flavor = 3 (DES), len = 12 bytes          
-> Le vérificateur
          RPC:      Timestamp = 0x14526973ABCD55CC (DES encrypted)
          RPC:       Window = 0xA4509B65 (DES encrypted)
Dans le vérificateur de la réponse du client, on trouve le nickname :
 RPC:  ----- SUN RPC Header -----
          RPC:
          RPC: Transaction id = 1874964582
          RPC:  Type = 1 (Reply)
          RPC:   Status = 0 (Accepted)
          RPC: Verifier       : Flavor = 3 (DES), len = 12 bytes
          RPC:     Timestamp = 0x568741D6B5A8C431 (DES encrypted)
          RPC:      Nickname = 0x40000000          
-> le reply contient le nickname pour cette session
          RPC: Accept status = 0 ( Success)
Ensuite, les messages du client utilisent le nickname obtenu :
 RPC:  ----- SUN RPC Header -----
          RPC:
          RPC:  Transaction id = 1874952746
          RPC:  Type = 0 (Call)
          RPC:  RPC version = 2
          RPC:  Program = 100300 (NIS+), version = 3, procedure = 5
          RPC:  Credentials: Flavor = 3 (DES), len = 8 bytes  
-> les credentials ne contiennent pas le nom complet et sont plus courts
          RPC:     Name kind = 1 (nickname)                   
->  On utilise le nickname que l'on a obtenu lors du premier appel
          RPC:     Nickname   = 0x40000000
          RPC:  Verifier       : Flavor = 3 (DES), len = 12 bytes
          RPC:      Timestamp = 0x5671BCAD678900AA (DES encrypted)
          RPC:       Window = 0x00000000 (DES encrypted)

2.5 Authentification avec Kerberos 4 : AUTH_KERB

Dans ce qui suit, nous supposons que le lecteur est familier avec les notions relatives à Kerberos, en particulier les mécanismes basés sur une tierce personne de confiance. Nous ne rentrerons donc dans aucun détail relatif à Kerberos. L'utilisation de Kerberos 4 dans AUTH_KERB est très similaire à ce qui se fait avec AUTH_DH. La différence entre AUTH_DH et AUTH_KERB tient essentiellement dans la connaissance d'informations très utiles déjà obtenues durant la négociation du ticket Kerberos. En particulier, on en déduit non seulement le nom du client (le netname dans la sémantique de AUTH_DH est très similaire à la notion de principal de Kerberos), mais aussi la clef de conversation qui sera en fait la clef de session au sens de Kerberos. Ce dernier point nous libère de la faiblesse de AUTH_DH relative à l'obtention des clefs publiques. L'authentification AUTH_KERB fonctionne à peu de choses près de la même façon que AUTH_DH si ce n'est que l'ensemble { netname ; clef de conversation chiffrée } est remplacé par le ticket Kerberos, chiffré selon la méthode de Kerberos (donc DES dans la cadre de Kerberos 4). Comme dans AUTH_DH, les credentials sont accompagnés d'une fenêtre temporelle chiffrée et de vérificateurs eux aussi chiffrés, lesquels sont utilisés tout à la fois dans les requêtes et les réponses. Comme dans AUTH_DH, le serveur utilise un nickname pour identifier la session et éviter que le client utilise toujours son credential complet à chaque appel. Une fois que le client obtient un nickname, la structure de son credential est très simplifiée puisqu'il suffit de donner au serveur son nickname pour identifier la session de façon non équivoque. A la différence de AUTH_DH, un nouveau nickname est généré par le serveur à chaque fois, ce qui le protège contre de mauvais esprits qui chercheraient à réutiliser des nicknames qui ont déjà servi. Le vérificateur aura donc une structure très voisine au niveau du client et du serveur puisqu'il sera constitué d'une date chiffrée pour garantir qu'on est dans une fenêtre de temps acceptable et d'un nickname. La seule différence est que le nickname du client est le dernier que lui a accordé le serveur, alors que dans le cas du serveur, il s'agit d'un nouveau nickname.

2.6 Utilisation de Kerberos 5 et limitation de ONC/RPC

Si vous avez bien suivi ce qui précède, on procède toujours de la même façon quand il s'agit de gérer une authentification dans ONC/RPC. Le credential et le vérificateur sont des buffers opaques sur lesquels on plaque une structure spécifique au type d'authentification de façon à en déduire les informations utiles à l'authentification. A priori, on pourrait plaquer n'importe quelle structure sur le credential et le vérificateur pour faire des échanges d'informations entre le client et le serveur. Malheureusement il y a une limite forte : la taille du credential est limitée à 400 octets. Ce n'est pas gênant pour y mettre une clef Diffie-Hellman de 192 octets ou un ticket Kerberos 4, mais avec d'autres protocoles, comme Kerberos 5, la place vient à manquer. Or, pour des raisons évidentes de compatibilité avec l'existant, il est inenvisageable d'agrandir le champ cb_cred::oa_base de l'en-tête de message RPC. On est donc bel et bien coincé. C'est pourquoi il n'existe pas de AUTH_KRB5. Heureusement, une solution a été trouvée par le biais de RPCSEC_GSS qui est une extension de ONC/RPC incluant un support explicite de la GSS-API.

3. Contourner la limite de taille du credential :  définition de RPCSEC_GSS

RPCSEC_GSS s'appuie sur le formalisme de la GSS-API. Autant dire les choses comme elles sont : c'est un formalisme assez lourd mais qui apporte des avantages conséquents, le principal étant de gérer de façon portable différents mécanismes de sécurité, sans adhérence ou presque avec les spécificités du mécanisme. L'emploi de la GSS-API garantit ainsi que des protocoles tels que Kerberos 5 qui ne sont pas supportés, comme on l'a vu, par ONC/RPC, le seront dans RPCSEC_GSS, mais il permet aussi de garantir que les futurs mécanismes, supportant la GSS-API, seront supportés par RPCSEC_GSS. On peut toutefois utiliser RPCSEC_GSS sans connaître la GSS-API dans la mesure où son formalisme est très proche, du point de vue du développeur, de celui de ONC/RPC. Le fonctionnement interne, en revanche, est très différent. On notera cependant que RPCSEC_GSS est totalement compatible avec ONC/RPC : un serveur basé sur RPCSEC_GSS peut également implémenter tous les types d'authentification de ONC/RPC.

3.1 Rappel : La GSS-API en deux mots

La GSS-API , ou Generic Security Service Application Programming Interface est un modèle de programmation destiné à être implémenté comme une sur-couche d'un mécanisme de sécurité existant. La GSS-API standardise les structures, les fonctions à utiliser et la façon de les mettre en œuvre. Grâce à cette API générique, il est possible de gérer de l'authentification entre un client et un serveur en minimisant les adhérences avec le mécanisme de sécurité sous-jacent, ce qui apporte un gain de portabilité et de maintenabilité de l'application particulièrement appréciable. Dans la suite nous verrons comment la GSS-API est utilisée dans ONC/RPC pour faire RPCSEC_GSS. Nous n'allons pas entrer dans les détails concernant la GSS-API, je vous renvoie plutôt aux documents indiqués à la fin de cet article, sachez simplement que :
  • l'acquisition d'un credential au sens du mécanisme de sécurité se fait au travers de la fonction gss_acquire_cred, ou bien le credential est hérité de l'environnement (par exemple lancé depuis un shell qui dispose d'un ticket Kerberos) ;
  • le client et le serveur, qui disposent chacun de credentials GSS-API, vont partir dans une boucle de négociation, dans le but d'établir un contexte commun. Cette boucle peut présenter une ou plusieurs itérations, selon le type de mécanisme de sécurité sous-jacent. Dans celle-ci, le client appelle la fonction gss_init_sec_context, ce qui lui donne une " proposition de contexte " qu'il envoie au serveur. Le serveur reçoit cette information, la fait traiter par la fonction gss_accept_sec_context, et retourne le résultat au client. On repart alors sur une éventuelle autre itération jusqu'à converger sur l'établissement du contexte commun final ;
  • si le mécanisme sous-jacent le permet, la GSS-API fournit les outils nécessaires pour chiffrer/déchiffrer des messages (les fonction gss_wrap et gss_unwrap) ainsi que les fonctions pour créer une signature du message utile à la vérification de l'intégrité de celui-ci (gss_get_mic et gss_verify_mic ).
Il est à noter que la GSS-API est fournie en standard dans les implémentations de Kerberos 5. D'autres protocoles de sécurité fournissent également un support de la GSS-API, poussés par leur utilisation dans NFSv4 au travers de RCPSEC_GSS. On notera en particulier les protocoles LIPKEY (RFC2847) et SPKM-3 (RFC2025).

3.2 RPCSEC_GSS

ou comment les traditions viennent au secours des concepteurs de protocole On a vu que se limiter à l'emploi de l'en-tête du message ONC/RPC pour contenir les informations relatives à l'authentification ne suffisait pas, par manque de place. S'il est impossible d'utiliser la place disponible dans l'en-tête d'un message, le seul moyen restant consiste à mettre ces informations dans le corps du message. Le problème est que l'on ne doit pas venir marcher sur les plates-bandes des données de nature applicative qui sont dans le corps du message. La situation ressemble fort à une impasse. Heureusement, les architectes de RPCSEC_GSS ont trouvé un contournement, en s'appuyant sur un comportement traditionnel des produits utilisant les ONC/RPC. Mais avant de voir comment, faisons une petite digression. Quand un produit dispose d'un protocole basé sur les ONC/RPC, il définit un jeu de fonctions disposant d'arguments et de résultats au format bien connu et identifié de façon unique par un numéro. L'ensemble des couples (numéro de fonction, fonction de service associée) forme l'épine dorsale du traitement des requêtes dans un client ou un serveur RPC. Il est d'usage de toujours avoir une fonction dont le numéro est 0, dont le nom est en général PROC_NULL. Cette fonction ne prend aucun argument et ne retourne aucun argument, et d'ailleurs elle ne fait rien. Elle existe simplement pour vérifier que toute la mécanique des RPC est correctement en place. Appeler la fonction 0 d'un protocole revient à vérifier que le client et le serveur sont à même de dialoguer correctement l'un avec l'autre, que les ressources nécessaires sont en place et que l'on est par conséquent prêt à passer à des choses plus sérieuses. Dans un sens cette fonction est à un serveur RPC ce que la commande ping est à une interface réseau : un moyen simple de vérifier sa présence et son fonctionnement nominal. Si vous avez utilisé les ONC/RPC au moins une fois dans votre vie, vous connaissez la commande rpcinfo. Cette dernière vérifie la présence d'un serveur pour un service RPC donné. Ainsi " rpcinfo -u localhost nfs 3 " vous permet de vérifier qu'un serveur NFS v3 répond en UDP sur votre machine. Comme vous vous en doutez, rpcinfo utilise PROC_NULL pour faire ce test. Revenons à RPCSEC_GSS. Nous avons besoin de transmettre des messages relativement gros pour gérer l'authentification, trop gros pour tenir dans l'en-tête. Or, on dispose toujours d'une fonction PROC_NULL de numéro 0 car aucun développeur ne songerait à ne pas le faire par crainte d'être frappé d'anathème (il y a des us et coutumes qui ne sauraient être discutés). RPCSEC_GSS utilise tout simplement cette fonction PROC_NULL en la surchargeant afin de recevoir et émettre des données. On doit tout de même faire les choses proprement et distinguer un appel à PROC_NULL normal et un appel à PROC_NULL qui est en fait surchargé avec un message de négociation d'authenticité. Ce problème est très simplement résolu : puisque c'est le corps du message qui est mis à contribution, il nous reste de la place dans l'en-tête du message. On y place une étiquette qui indique si le message est un " vrai " message de données ou bien un message à usage interne à RPCSEC_GSS pour la mise en place de l'authentification. Pour ce qui est de l'interface avec le mécanisme de sécurité sous-jacent, RPCSEC_GSS utilise la GSS-API. Cela suppose la présence d'une boucle de négociation, comme cela a été décrit un peu plus haut. RPCSEC_GSS implémentera cette boucle en utilisant simplement un appel à PROC_NULL estampillé " message de négociation " à chaque fois qu'un échange est nécessaire. La GSS-API fournit des services de chiffrement et de génération de message d'intégrité, RPCSEC_GSS va les utiliser. De plus, insistons sur un point essentiel quant à RPCSEC_GSS qui le rend radicalement différent des autres mécanismes d'authentification : RPCSEC_GSS dépend uniquement de la GSS-API, et non du mécanisme de sécurité. Avec AUTH_DH et AUTH_KERB il y a une très forte adhérence, ne serait-ce qu'au niveau de l'interprétation du credential dans l'en-tête du message RPC. A chaque fois, un effort de standardisation est indispensable, avec à la clef la rédaction d'une RFC pour définir la façon de procéder. Avec RPCSEC_GSS, l'adhérence est uniquement avec la GSS-API ; il suffit donc qu'un mécanisme de sécurité fournisse un support de la GSS-API pour qu'automatiquement ce mécanisme puisse être utilisé pour sécuriser des RPC via RPCSEC_GSS.

3.3 Disponibilité de RPCSEC_GSS

RPCSEC_GSS est généralement disponible pour tout mécanisme de gestion de la sécurité qui supporte la GSS-API. Actuellement, on trouve la GSS-API pour Kerberos 5 ainsi que pour les protocoles SPKM-3 (RFC2025) et LIPKEY (w) (pour utilisation dans une implémentation de NFSv4). On trouve aussi sur certains OS, dont Solaris, des implémentations de la GSS-API et de RPCSEC_GSS au dessus de Diffie-Hellman avec des clefs de plus grande longueur (640 octets ou 1024 octets). Une authentification via RPCSEC_GSS est caractérisée par cb_cred::oa_flavor = 6 = constante " RPCSEC_GSS " . Dans la mesure où elle repose sur la GSS-API, qui est un standard ouvert sur lequel il est possible de plaquer n'importe quel type d'authentification, on considère que RPCSEC_GSS est la dernière authentification mise en place dans les RPC. Toutes les authentifications futures l'utiliseront en passant par leur interface GSS-API.

3.4 La boucle de négociation GSS-API, vue au travers de RPCSEC_GSS

RPCSEC_GSS présente, du point de vue de l'application cliente, une très grande similarité avec ONC/RPC. En gros, une application qui utilise les appels ONC/RPC utilisera RPCSEC_GSS de la même façon : création d'une structure CLIENT *, puis instanciation du champ de cette structure propre à l'authentification. Quand un client initie une authentification avec un serveur RPCSEC_GSS, celui-ci utilise la socket UDP ou TCP de la structure CLIENT * nouvellement initiée pour y envoyer un message de demande de négociation en utilisant la procédure 0 du protocole (PROC_NULL). Pour que le serveur sache qu'il ne s'agit pas d'un paquet contenant des données mais d'un message propre à l'implémentation des routines de RPCSEC_GSS, on doit mettre une sorte d'étiquette pour différencier les deux cas. On utilisera le champ credential du message RPC qui contiendra une structure rpc_gss_cred, décrivant les credentials RPCSEC_GSS. Cette structure contient :
  • gc_v : La version de RPCSEC_GSS utilisée (en pratique, toujours la valeur 1, ce champ existe pour permettre de futures extensions de protocole) ;
  • gc_proc : la procédure de contrôle. Peut prendre les valeurs suivantes :
    • RPCSEC_GSS_DATA : le message n'est pas un paquet de négociation ;
    • RPCSEC_GSS_INIT : le message est une initialisation de négociation ;
    • RPCSEC_GSS_CONTINUE_INIT : le message est une phase intermédiaire de négociation ;
    • RPCSEC_GSS_DESTROY : le message indique la destruction d'un contexte précédemment négocié.
  • gc_seq : numéro de séquence. Cette valeur sert à calculer une somme de contrôle à l'aide du contexte négocié entre les acteurs. La somme résultante est placée dans le vérificateur de la réponse à une requête. Ce moyen garantit que la réponse corresponde bien à l'acteur concerné et qu'il ne s'agisse pas d'une réponse falsifiée.
  • gc_svc : nature du service de RPCSEC_GSS utilisé :
    • RPCSEC_GSS_SVC_NONE : on authentifie, mais on ne vérifie pas l'intégrité des messages ;
    • RPCSEC_GSS_SVC_INTEGRITY : authentification plus vérification de l'intégrité du message par calcul d'une MIC (somme de contrôle) gérée par la GSS-API ;
    • PCSEC_GSS_SVC_PRIVACY : authentification et échange de messages chiffrés.
  • gc_ctx : contexte GSS-API propre au dialogue client serveur.
Lors d'une négociation, les choses vont se passer de la façon suivante :
  • 1. le client initie un contexte de sécurité en appelant gss_init_sec_context.
Il forme un message avec l'étiquette gc_proc = RPCSEC_GSS_INIT puis stocke l'amorce de contexte dans le corps du message RPC comme une donnée utilisateur. Le message est envoyé en surchargeant la fonction.
  • 2. Le serveur reçoit ce paquet, voit l'étiquette gc_proc = RPCSEC_GSS_INIT et sait qu'il s'agit d'un appel à la fonction 0 surchargée.
Il décode le contexte et l'utilise comme argument pour gss_accept_sec_context. Il retourne le résultat (toujours dans le corps du message de la fonction 0) dans la réponse au client. Si le contexte est négocié avec succès, on pourra l'utiliser ultérieurement, sinon une autre passe est nécessaire.
  • 3. Si une autre passe est nécessaire, le client emploie à nouveau gss_init_sec_context en utilisant la réponse du serveur et envoie le résultat avec une étiquette RPCSEC_GSS_CONTINUE_INIT.
Et ainsi de suite jusqu'à établissement du contexte ou échec dans la négociation.
Par la suite, le serveur pourra utiliser normalement les fonctions de son protocole, mais le champ gc_proc vaudra RPCSEC_GSS_DATA, ce qui indique qu'il s'agit d'un message usuel et pas d'un message de contrôle lié à la gestion des contextes.

3.5 Différents types de services de RPCSEC_GSS

Comme cela a été évoqué plus haut, RPCSEC_GSS supporte trois types de services différents :
  • Le premier, RPCSEC_GSS_SVC_NONE indique que l'on utilise la GSS-API uniquement pour gérer l'authentification entre le client et le serveur. Ensuite, des messages similaires à ceux de ONC/RPC sont utilisés. Cela suppose que les appels RPC sont faits sur un protocole de transport comme TCP, orienté connexion.
  • Le service RPCSEC_GSS_SVC_INTEGRITY intègre au message la notion d'intégrité. Il permet de vérifier si le message est bien identique au niveau du client et du serveur, mais aussi de vérifier que le message vient bien de celui avec lequel on a négocié un contexte. En effet, le calcul de la somme de contrôle (ou MIC) dépend du contexte qui n'est connu que du client et du serveur. Ce service repose sur les fonctions gss_get_mic et gss_verify_mic de la GSS-API.
  • Le service RPCSEC_GSS_SVC_PRIVACY permet de déchiffrer le message en totalité, en se basant sur les fonctions gss_wrap / gss_unwrap de la GSS-API. Seuls les acteurs ayant connaissance du contexte négocié sont à même de décoder son contenu.
La GSS-API, qui sous-tend RPCSEC_GSS, gère des sommes de contrôle (ou MIC) pour gérer l'intégrité des messages, mais aussi permet l'échange de messages chiffrés. Il était logique que RPCSEC_GSS fournisse (quand le système de sécurité sous-jacent l'implémente) des outils similaires. Dans le cas de la gestion de l'intégrité, les informations à partir desquelles calculer une somme de contrôle sont soit les arguments d'une fonction RPC (dans le cas d'une requête), soit le résultat de la fonction (dans le cas d'une réponse). Dans ce cas, le corps du message change puisqu'il doit inclure la somme de contrôle. On plaquera donc le corps du message RPC sur une structure dont la déclaration est la suivante (définie ici en RPCL, le langage compilé par l'utilitaire rpcgen) :
struct rpc_gss_integ_data {
opaque <> databody_integ ;
opaque <> checksum;}
Le champ databody_integ est lui-même plaqué sur la structure suivante :
struct rpc_gss_data {
uint32 seq_num ;
proc_req_arg arg ;}
Le récepteur du message calculera la somme de contrôle sur la structure databody_integ. Si elle est valide, alors on vérifie que le numéro de séquence seq_num est bien le même que dans les credentials du message. Si tout est correct, on exploite proc_req_arg qui contient le message à proprement parler, encodé par XDR, comme cela est fait avec les ONC/RPC. La génération du message est faite en sens inverse : le message est encodé en XDR, on y ajoute le numéro de séquence puis on calcule la MIC. L'emploi de messages complètement chiffrés est réalisable de la même façon. Cette fois-ci, ce sont les fonctions gss_wrap et gss_unwrap qui sont utilisées de façon interne par l'implémentation de RPCSEC_GSS. Les clefs et la " quincaillerie " nécessaires au chiffrement ont été négociées dans la phase initiale, lors de l'authentification du client sur le serveur et sont donc contenues dans le contexte GSS-API des deux acteurs. Pour lire un message chiffré, on plaque dessus la structure suivante :
struct rpc_gss_priv_data {
  opaque <> databody_priv ;}
Ensuite, on déchiffre le champ databody_priv avec gss_unwrap. En cas de succès, on obtient un résultat qui est de type rpc_gss_data, décrit quelques lignes au-dessus. On vérifie la cohérence du numéro de séquence avant de décoder le corps du message à l'aide de XDR. Il faut noter que tout ce processus complexe est transparent à l'utilisateur ainsi qu'au développeur final de l'application. Dans le cas de ONC/RPC, l'emploi de XDR est fait en interne des routines de la libC, sans intervention explicite de l'utilisateur. Dans le cas des fonctionnalités cryptographiques de RPCSEC_GSS, on intercale juste la phase chiffrement/déchiffrement avant le passage par XDR, exactement dans la même logique.

3.6 Trace réseau

Voici le type de trace réseau que l'on obtient avec RPCSEC_GSS. Le service qui est appelé ici est celui d'une programme de test, qui ne couvre aucune fonction particulière à part celle d'utiliser RPCSEC_GSS. Le numéro de programme utilisé est 88888888. Lors de la création du contexte d'authentification, on a vu qu'un appel à la procédure 0, avec gc_proc = RPCSEC_GSS_INIT était engendré, contenant le contexte en cours de négociation dans le corps du message. On verra ainsi la trace suivante :
 RPC:  ----- SUN RPC Header -----
          RPC:
          RPC:  Transaction id = 8569412567
          RPC:  Type = 0 (Call)
          RPC:  RPC version = 2
          RPC:  Program = 88888888 (?), version = 1, procedure = 0
          RPC:   Credentials: Flavor = 6 (RPCSEC_GSS), len = 20 bytes
          RPC:           version = 1
          RPC:           gss control procedure = 1 (RPCSEC_GSS_INIT) 
--> surcharge de RPC_NULL par RPCSEC_GSS_INIT
          RPC:           sequence num = 0
          RPC:           service = 1 (none)                          
--> utilisation de RPCSEC_GSS_SVC_NONE
          RPC:           handle:  length = 0, data = []
          RPC:  Verifier        : Flavor = 0 (None), len = 0 bytes
          RPC:
          RPC:  RPCSEC_GSS_INIT args:
          RPC:          gss_token: length = 502 bytes
          RPC:   (on trouve ensuite le message de négociation créé avec gss_init_sec_context)
Le serveur retourne, dans la réponse, le message de négociation, retraité par ses soins avec gss_accept_sec_context :
 RPC:  ----- SUN RPC Header -----
          RPC:
          RPC: Transaction id = 8569412567
          RPC:  Type = 1 (Reply)
          RPC:   Status = 0 (Accepted)
          RPC: Verifier       : Flavor = 6 (RPCSEC_GSS), len = 37 bytes
          .....
          RPC: RPCSEC_GSS_INIT result:
          RPC:           handle:   length = 4, data = [00000003]  
--> le serveur a négocié un contexte, il retourne un identifiant qui indexe ce contexte
          RPC:           gss_major status    = 0                  
-->  gss_major = gss_minor = 0, la négociation est un succès, le contexte négocié est dans le corps de la réponse
          RPC:           gss_minor status    = 0
          RPC:           sequence window = 128
          RPC:            gss token:   length = 106 bytes
          RPC:    ..... les données contenues dans le gss token,
          résultat de la négociation .....
Par la suite, les appels du client ont la forme suivante :
RPC:  ----- SUN RPC Header -----
          RPC:
          RPC:  Transaction id = 8569412678
          RPC:  Type = 0 (Call)
          RPC:  RPC version = 2
          RPC:  Program = 88888888 (?), version = 1, procedure = 1
          RPC:   Credentials: Flavor = 6 (RPCSEC_GSS), len = 24
          bytes
          RPC:           version = 1
          RPC:           gss control procedure = 1 (RPCSEC_GSS_DATA)     
--> un appel normal, pas une fonction de contrôle
          RPC:           sequence num = 2
          RPC:           service = 1 (none)
          RPC:           handle:  length = 4, data = [00000003]          
--> le client rappelle au serveur l'identifiant du contexte négocié pour permettre au serveur de le retrouver
          RPC:  Verifier        : Flavor = 6 (None), len = 37 bytes
          RPC:     ...... somme de contrôle du numéro de séquence .....  
--> le serveur calculera la cohérence entre seq_num et cette MIC avec les informations de contexte, pour authentifier le message
          RPC:
          RPC:  ...... ensuite on trouve les informations relatives à la procédure 1 du protocole program = 88888888, vers = 1

5. Conclusion en forme de résumé

L'authentification est un besoin qui a été pris en compte dans les RPC dès la définition du protocole. Par la suite, des mécanismes de plus en plus complexes ont été ajoutés au standard au fur et à mesure que les technologies et les besoins évoluaient, jusqu'à faire apparaître des limitations structurelles dans le protocole. Ces limitations ont été à l'origine de RPCSEC_GSS, protocole plus évolué et ne rencontrant pas les limitations de ONC/RPC, tout en restant compatible avec ce dernier. Le support, la GSS-API en interne de RPCSEC_GSS, garantit le futur du protocole dans la mesure où il utilise une API standard présente désormais dans les protocoles de sécurité actuels (Kerberos 5, mais aussi SPKM et LIPKEY). Quand l'IETF a posé les définitions de NFSv4, la sécurité et l'authentification à différentes échelles (du LAN au WAN) sont apparues comme un besoin prioritaire. RPCSEC_GSS est né de ce besoin, pour pallier les défauts de ONC/RPC en la matière. Les ONC/RPC avaient vu le jour pour servir de couche de transport aux protocoles NFSv2 et NFSv3. Ainsi, de même que NFS avait " porté " ONC/RPC, NFSv4 doit porter RPCSEC_GSS car toute implémentation de ce protocole doit explicitement s'appuyer sur ce nouveau standard de RPC. De plus, RPCSEC_GSS est formellement très proche de ONC/RPC, tant au niveau du codage des clients que de celui des serveurs, une similarité qui facilite le portage des applicatifs existants dans ce nouveau protocole tout en assurant son évolutivité avec les protocoles de sécurité futurs puisque les adhérences se limitent à la GSS-API dont les spécifications sont fixées une fois pour toutes. De même que NFSv4, RPCSEC_GSS est un standard émergent, en train de gagner du terrain petit à petit. Si la sécurité n'est pas un besoin critique de vos outils, RPCSEC_GSS ne présente que peu d'intérêt et amène un certain surcoût. Dans le cas contraire, c'est la solution naturelle à adopter, pour sa généricité, sa portabilité et son évolutivité. Références :
  • RFC1831 : Remote Procedure Call Version 2 
  • RFC1832 : XDR: External Data Representation Standard
  • RFC2203 : RPCSEC_GSS Protocol Specification
  • RFC2478 : The simple and Protected GSS-API Negociation Mechanism
  • RFC2623 : NFS Version 2 and Version 3 Security Issues and the NFS Protocol's Use of RPCSEC_GSS and Kerberos V5RFC3530 : Network File System (NFS) version 4 Protocol
  • RFC1509 : Generic Security Service API: C-bindings
  • RFC1510 : The Kerberos Network Authentication Service (V5)
  • RFC1964 : The Kerberos Version 5 GSS_API Mechanism
  • RFC2078 : Generic Security Service Application Programming Interface, Version 2
  • RFC2025 : The Simple Public-Key GSS-API Mechanism (SPKM)
  • RFC2847 : A Low Infrastructure Public Key Mechanism Using SPKM
  • Premiers pas avec la GSS-API, Linux Magazine numéro 52.
  • Article Authentification : clé de voûte de la confiance, ManuX, Misc 15.

Retrouvez cet article dans : Misc 17

Il y a : 0 commentaire(s)

Donnez votre avis

Vous devez avoir ouvert une session pour écrire un commentaire.

Brèves
Édito : Linux Pratique Essentiel N°24
Édito : Linux Pratique HS N°23
Édito : GNU/Linux Magazine 146
Édito : GNU/Linux Magazine HS N°58
Édito : Open Silicium N°5
Communication
Linux Pratique HS 23 – Communiqué de presse
Linux Pratique Essentiel N°24 – Communiqué de presse
Gnu/Linux Magazine sponsor et partenaire de PROLOGIN
Linux Essentiel partenaire des Rencontres du Libre de Lion sur Mer (Normandie)
GNU/Linux Magazine HS 58 – Communiqué de presse
prochainement moteur de recherches des articles
 
:
:
Jours heures minutes secondes
En kiosque
Le tout nouveau Linux Pratique Essentiel est disponible dès maintenant chez votre marchand de journaux et sur notre site...

Lire la suite...

Le tout nouveau Linux Pratique est disponible dès maintenant chez votre marchand de journaux et sur notre site...

Lire la suite...

Le tout nouveau GNU/Linux Magazine est disponible dès maintenant chez votre marchand de journaux et sur notre site...

Lire la suite...

Le tout nouveau GNU/Linux Magazine HS est disponible dès maintenant chez votre marchand de journaux et sur notre site...

Lire la suite...

Le tout nouveau Open Silicium est disponible dès maintenant chez votre marchand de journaux et sur notre site...

Lire la suite...

Le tout nouveau Linux Pratique est disponible dès maintenant chez votre marchand de journaux et sur notre site...

Lire la suite...

Le tout nouveau Misc est disponible dès maintenant chez votre marchand de journaux et sur notre site...

Lire la suite...

Le tout nouveau GNU/Linux Magazine est disponible dès maintenant chez votre marchand de journaux et sur notre site...

Lire la suite...