Authentification mutuelle et ressortingction des certificates utilisateur à un ensemble spécifique sur le serveur

Je cherche un moyen de limiter les certificates clients à un ensemble spécifique de certificates auto-signés du côté serveur à l’aide de l’API OpenSSL.

Il existe un ensemble de certificates auto-signés approuvés, par exemple ./dir/*.pem . Je veux rejeter les connexions si elles ne fournissent pas l’un de ces certificates.

Je peux réaliser le comportement souhaité en comparant les empreintes de certificates serveur et client dans le rappel de vérification du contexte SSL:

 SSL_CTX *ctx; ... SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, verify_callback); static inline int get_fingerprint(X509* cert, unsigned char *md, unsigned int *n) { return X509_digest(cert, EVP_sha1(), md, n); } static inline int compare_certificatees(X509 *c1, X509 *c2) { unsigned char md1[EVP_MAX_MD_SIZE], md2[EVP_MAX_MD_SIZE]; unsigned int n1, n2; if (!(get_fingerprint(c1, md1, &n1) && get_fingerprint(c2, md2, &n2))) { return -1; } return memcmp(md1, md2, n1); } static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { SSL *ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()); int err = X509_STORE_CTX_get_error(ctx); /* Allow self-signed certificatees */ if (!preverify_ok && err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT) { preverify_ok = 1; } if (0 != compare_certificatees(ctx->current_cert, SSL_CTX_get0_certificatee(ssl->ctx))) { /* Peer certificatee doesn't match the server certificatee */ preverify_ok = 0; } /* More checks ... */ return preverify_ok; } 

Donc, si les empreintes digitales du certificate du serveur et du client correspondent, la vérification est réussie. Sinon, la connexion est fermée par le serveur.

Je pourrais calculer les empreintes digitales des certificates de confiance quelque part en phase d’initialisation, puis les vérifier dans une boucle dans le verify_callback . Cependant, je n’aime pas cette idée. Il devrait y avoir un moyen plus facile de le faire.

Je pensais que SSL_CTX_load_verify_locations() était exactement ce que je cherchais ( mais il semble que ce ne soit pas le cas; je vais expliquer pourquoi ):

SSL_CTX_load_verify_locations () spécifie les emplacements pour ctx, sur lesquels se trouvent les certificates CA à des fins de vérification. … Si CAfile n’est pas NULL, il pointe vers un fichier de certificates CA au format PEM. Le fichier peut contenir plusieurs certificates d’autorité de certificateion … Les certificates dans CApath ne sont recherchés que lorsque cela est nécessaire, par exemple lors de la création de la chaîne de certificates ou lors de la vérification d’un certificate homologue.

( man 3 SSL_CTX_load_verify_locations )

Eh bien, je suppose que SSL_VERIFY_FAIL_IF_NO_PEER_CERT implique la vérification du certificate homologue. Ensuite, il me semble que tout ce que je dois faire est de créer un paquet de certificates approuvés et de le transmettre à SSL_CTX_load_verify_locations() :

 bundle_file=CAbundle.pem cd ./dir rm -f $bundle_file for i in *.pem; do openssl x509 -in $i -text >> $bundle_file done c_rehash . 
 SSL_CTX *ctx; const char *cafile = "dir/CAbundle.pem"; const char *capath = NULL; ... if (!SSL_CTX_load_verify_locations(ctx, cafile, capath)) { /* Unable to set verify locations ... */ } cert_names = SSL_load_client_CA_file(cafile); if (cert_names != NULL) { SSL_CTX_set_client_CA_list(ctx, cert_names); } else { /* Handle error ... */ } 

Tout va bien. Mais le serveur accepte toujours les connexions avec différents certificates homologues.

J’ai reproduit ce comportement en utilisant les utilitaires OpenSSL standard ici: https://gist.github.com/rosmanov/d960a5d58a96bdb730303c5b8e86f951

Ma question est donc la suivante: comment configurer le serveur pour qu’il accepte uniquement les pairs ne fournissant que des certificates spécifiques?

Mettre à jour

J’ai constaté que la “liste blanche” des certificates (ensemble CA) fonctionne réellement lorsque je supprime les éléments suivants de verify_callback :

 if (!preverify_ok && err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT) { preverify_ok = 1; } 

Donc, sans ce bloc, tout fonctionne. Le serveur répond à un client connecté à l’un des certificates répertoriés dans CAbundle.pem . Si un client se connecte avec un certificate différent, le serveur ferme la connexion.

Cependant, il y a une chose étrange. Dans les deux cas, openssl s_client :

 Verify return code: 18 (self signed certificatee) 

Alors peut-être

 if (!preverify_ok && err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT && allow_self_signed && !cafile && !capath) { preverify_ok = 1; } 

?

Mise à jour 2

Maintenant, je comprends pourquoi openssl s_client sorties Verify return code: 18 (self signed...) . Il ne fait pas confiance au certificate du serveur, sauf si -CAfile ou -CApath contient le certificate du serveur. Et le certificate du serveur est auto-signé.

Une explication (pour la ligne de commande) et une demi-réponse (pour la bibliothèque):

J’ai (cette fois entièrement) refait ton esprit et on m’a rappelé une incohérence ici. Les utilitaires openssl xxx sont principalement conçus comme des outils de test / débogage, et en particulier:

  • s_client normalement (sauf anonyme, SRP, etc.) reçoit une chaîne de cert du serveur, mais utilise un callback qui enregistre uniquement ce qu’il a obtenu et ignore / remplace toute erreur ; c’est le bloc

     depth=0 C = AU, ST = StateA, L = CityA, O = CompanyA, CN = localhost, emailAddress = [email protected] verify error:num=18:self signed certificatee verify return:1 depth=0 C = AU, ST = StateA, L = CityA, O = CompanyA, CN = localhost, emailAddress = [email protected] verify return:1 

    juste après CONNECTED(fd) dans votre sortie s_client , mais comme vous le voyez malgré l’erreur, il continue avec le handshake résultant en une connexion utilisable.

  • s_server est un peu plus compliqué. Il ne demande pas de cert du client par défaut, seulement si vous spécifiez -verify ou -Verify (qui définit SSL_VERIFY_PEER qui n’est pas la valeur par défaut pour le serveur), et s’il demande un client cert a l’ option d’envoyer un preuve associée dans CertVerify). Si le client envoie une chaîne, s_server utilise le même rappel que s_client qui remplace toute erreur et continue la connexion. ceci dans votre sortie s_server avec la même verify error:num-18... ce qui signifie en réalité “root (y compris auto-signé qui est sa propre racine) dans la chaîne reçue mais pas dans truststore local”. Si le client n’envoie pas de chaîne, -verify continue, mais -Verify (qui définit également SSL_VERIFY_FAIL_IF_NO_PEER_CERT ) SSL_VERIFY_FAIL_IF_NO_PEER_CERT la prise de contact avec l’alerte 40 et renvoie une erreur. s_server sortie de s_server est donc très différente:

     vérifier que la profondeur est 0, doit retourner un certificate
     Utilisation des parameters DH par défaut
     Utilisation des parameters temp ECDH par défaut
     ACCEPTEZ
     ERREUR
     140679792887624: erreur: 140890C7: routines SSL: SSL3_GET_CLIENT_CERTIFICATE: homologue n'a pas renvoyé de certificate: s3_srvr.c: 3271:
     arrêter SSL
     CONNEXION FERMÉE
     ACCEPTEZ

Mais un programme utilisant la bibliothèque devrait fonctionner . J’ai piraté ce test simple à partir de parties d’autres programmes (d’où l’indentation):

 / * SO36821430 2016-04-25 * /

 #include 
 #si défini (_WIN32) &&! defined (WIN32)
 #définir WIN32 / * n'importe quoi * /
 #fin si
 #ifdef WIN32
   #include 
   typedef int socklen_t;
   #define SOCKERR WSAGetLastError ()
   #include "openssl / applink.c"
 #autre
   #include 
   #include 
   #include 
   #include 
   #ifndef INADDR_NONE
   #define INADDR_NONE (ipaddr_t) -1
   #fin si
   typedef int SOCKET;
   enum {INVALID_SOCKET = -1, SOCKET_ERROR = -1};
   #define SOCKERR errno
   #define closeocket fermer
 #fin si

 #include "openssl / ssl.h"
 #include "openssl / err.h"
 #include "openssl / rand.h"

 void sockerr (const char * what) {
   fprintf (stderr, "% s% d% s \ n", quoi, SOCKERR, strerror (SOCKERR));
 }
 void sslerrn (const char * what) {
   fprintf (stderr, "*% s échoué: \ n", quoi);
   ERR_print_errors_fp (stderr);
 }
 void sslerr (const char * quoi, int rv) {
   fprintf (stderr, "*% s retourne% d: \ n", quoi, rv);
   ERR_print_errors_fp (stderr);
 }
 Annuler sslerrx (SSL * ssl, const char * quoi, int rv) {
   int rc = SSL_get_error (ssl, rv);
   if (rv == -1 && rc == SSL_ERROR_SYSCALL) sockerr (quoi);
   sinon fprintf (stderr, "*% s retourne% d,% d \ n", quoi, rv, rc);
   ERR_print_errors_fp (stderr); 
 }
 void subj_oneline (X509 * cert, FILE * fp) {
   X509_NAME * subj = X509_get_subject_name (cert);
   BIO * bmem = BIO_new (BIO_s_mem ());  char * ptr;  int n;
   X509_NAME_print_ex (bmem, subj, 0, XN_FLAG_ONELINE); 
   n = (int) BIO_get_mem_data (bmem, & ptr);
   si (n <= 0) ptr = "?", n = 1;
   fwrite (ptr, 1, n, fp);
 }

 const char * inaddr;
 int inport;
 char buf [9999];

 int main (int argc, char * argv [])
 {
   int rv; 
   struct sockaddr_in sin;  socklen_t sinlen;
   SOCKET SI, S2;  SSL_CTX * ctx = NULL;
   time_t maintenant;  struct tm * tm;
 #ifdef WIN32
   struct WSAData wsa;
   rv = WSAStartup (MAKEWORD (1,1), & wsa);
   if (rv) {printf ("WSAStartup% d \ n", rv);  sortie (1);  }
 #fin si

   if (argc <2 || argc> 6)
     printf ("utilisation:% s clé de port cert CAcerts \ n", argv [0]), exit (1);
   sin.sin_addr.s_addr = INADDR_ANY;
   sin.sin_port = htons (atoi (argv [1]));
   sin.sin_family = AF_INET;

   / ** /
     SSL_library_init ();
     SSL_load_error_ssortingngs ();
     ctx = SSL_CTX_new (SSLv23_server_method ());
     if (! ctx) {sslerrn ("CTX_new");  sortie (1);  }
     SSL_CTX_set_options (ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
     rv = SSL_CTX_use_PrivateKey_file (ctx, argv [2], SSL_FILETYPE_PEM);
     if (rv! = 1) {sslerr ("use_PrivateKey_file", rv);  sortie (1);  }
     rv = SSL_CTX_use_certificatee_file (ctx, argv [3], SSL_FILETYPE_PEM);
     if (rv! = 1) {sslerr ("use_certificatee_file", rv);  sortie (1);  }
     SSL_CTX_set_verify (ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); 
     if (! SSL_CTX_load_verify_locations (ctx, argv [4], NULL)) {
       sslerrn ("load_verify_locations");  sortie (1);  }
     SSL_CTX_set_client_CA_list (ctx, SSL_load_client_CA_file (argv [4]));
   / ** /
   if ((s1 = socket (AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
     sockerr ("socket ()");  sortie (1);  }
   if (bind (s1, (struct sockaddr *) & sin, sizeof sin) <0) {
     sockerr ("bind ()");  sortie (1);  }
   si (écoutez (s1, 5) <0) {
     sockerr ("listen ()");  sortie (1);  }

   faire{
     sinlen = taille du péché;
     if ((s2 = accept (s1, (struct sockaddr *) & sin, & sinlen)) == INVALID_SOCKET) {
       sockerr ("accept ()");  sortie (1);  }
     now = time (NULL);  tm = localtime (& now);
     printf ("+% s% u @% 02d.% 02d.% 02d \ n", inet_ntoa (sin.sin_addr),
       ntohs (sin.sin_port), tm-> tm_hour, tm-> tm_min, tm-> tm_sec);

     / ** /
       SSL * ssl = SSL_new (ctx);
       if (! ssl) {sslerrn ("SSL_new");  aller ensuite;  }
       SSL_set_fd (ssl, s2);
       if ((rv = SSL_accept (ssl)) <0) {
         sslerrx (SSL, "SSL_accept", rv);  aller ensuite;  }
       {X509 * cert = SSL_get_peer_certificate (ssl);
         / * EVP_PKEY * key = cert?  X509_get_pubkey (cert): NULL; * /
         fprintf (stdout, "=% ld", SSL_get_verify_result (ssl));
         if (cert) putchar (':'), subj_oneline (cert, stdout);
         putchar ('\ n');
       }
       while ((rv = SSL_read (ssl, buf, sizeof buf))> 0)
         printf ("% d:%. * s \ n", rv, rv, buf);
       sslerrx (ssl, "SSL_read", rv);
 prochain:
       if (ssl) SSL_free (ssl);
     / ** /
     now = time (NULL);  tm = localtime (& now);
     printf ("-% s% u @% 02d.% 02d.% 02d \ n", inet_ntoa (sin.sin_addr),
       ntohs (sin.sin_port), tm-> tm_hour, tm-> tm_min, tm-> tm_sec);
     closureocket (s2);
   } tant que (1);
   retourne 0;
 }

Lorsqu’elle est exécutée avec $port cert1.key cert1.pem CAbundle.pem et connectée à partir du client à l’aide de cert2.key & cert2.pem, cela interrompt la prise de contact avec l’alerte 48 unknown_ca et renvoie une erreur si nécessaire:

 + 127.0.0.1 46765 @22.07.36 * SSL_accept return -1,1 140240689366696:error:14089086:SSL routines:ssl3_get_client_certificatee:certificatee verify failed:s3_srvr.c:3270: - 127.0.0.1 46765 @22.07.36 

HTH

Si vous voulez une liste blanche de certificates clients spécifiques, vous pouvez préparer une liste indexée en mémoire lors de l’initialisation.

Par exemple, vous pouvez utiliser PEM_X509_INFO_read pour lire un fichier concaténé de tous les certificates clients au format PEM. Cela vous donnera un STACK_OF(X509_INFO)* des certificates. Le nombre de certificates peut être trouvé avec sk_X509_INFO_num , et vous pouvez voir chaque certificate à sk_X509_INFO_value(..)->x509 .

Ensuite, par exemple, vous pouvez simplement créer un index en mémoire et qsort par compare_x509 .

Maintenant, lorsque votre rappel de vérification est appelé, faites simplement une bsearch sur votre index par compare_x509 , et le certificate est sur votre liste blanche, ou ce n’est pas le cas.

Vous pouvez accepter la correspondance sur le résultat de compare_x5099 ou bien, bien sûr, vous pouvez vérifier en vérifiant le certificate complet une fois que la recherche a trouvé une correspondance dans l’index.