Bug #17208 SSL: client does not verify server certificate
Submitted: 7 Feb 2006 22:16 Modified: 13 May 2006 5:05
Reporter: Jorj Bauer Email Updates:
Status: Closed Impact on me:
None 
Category:MySQL Server Severity:S1 (Critical)
Version:5.0.18, 5.1-bk OS:
Assigned to: Magnus Blåudd

[7 Feb 2006 22:16] Jorj Bauer
Description:
The SSL code (in viosslfactories.c) explicitly sets the certificate validation to NONE. This means that a client may be talking to a server other than the one intended (man in the middle attack).

How to repeat:
Use an invalid cert on the server. Client will still allow it.

Suggested fix:
The following patch corrects this behavior. The patch was generated against 4.1.12; the problem also exists in 5.0.18.

diff --unified --recursive mysql-4.1.12/include/violite.h mysql-4.1.12.patched/include/violite.h
--- mysql-4.1.12/include/violite.h	2005-05-13 07:32:21.000000000 -0400
+++ mysql-4.1.12.patched/include/violite.h	2006-02-01 12:36:16.000000000 -0500
@@ -23,7 +23,9 @@
 #define	vio_violite_h_
 
 #include "my_net.h"			/* needed because of struct in_addr */
-
+#include "mysql.h"                      /* needed to get options, and hostname,
+					   down to the SSL layer for cert 
+					   validation -- jorj@isc.upenn.edu */
 
 /* Simple vio interface in C;  The functions are implemented in violite.c */
 
@@ -109,13 +111,15 @@
   SSL_CTX *ssl_context;
   /* function pointers which are only once for SSL client */ 
   SSL_METHOD *ssl_method;
+  MYSQL *mysql; /* so we can get the hostname later -- jorj@isc.upenn.edu */
 };
 
 int sslaccept(struct st_VioSSLAcceptorFd*, Vio *, long timeout);
 int sslconnect(struct st_VioSSLConnectorFd*, Vio *, long timeout);
 
 struct st_VioSSLConnectorFd
-*new_VioSSLConnectorFd(const char *key_file, const char *cert_file,
+*new_VioSSLConnectorFd(MYSQL *mysql,
+		       const char *key_file, const char *cert_file,
 		       const char *ca_file,  const char *ca_path,
 		       const char *cipher);
 struct st_VioSSLAcceptorFd
diff --unified --recursive mysql-4.1.12/sql-common/client.c mysql-4.1.12.patched/sql-common/client.c
--- mysql-4.1.12/sql-common/client.c	2005-05-13 07:32:04.000000000 -0400
+++ mysql-4.1.12.patched/sql-common/client.c	2006-02-01 12:36:16.000000000 -0500
@@ -1991,7 +1991,8 @@
     }
     /* Do the SSL layering. */
     if (!(mysql->connector_fd=
-	  (gptr) new_VioSSLConnectorFd(options->ssl_key,
+	  (gptr) new_VioSSLConnectorFd(mysql,
+				       options->ssl_key,
 				       options->ssl_cert,
 				       options->ssl_ca,
 				       options->ssl_capath,
diff --unified --recursive mysql-4.1.12/vio/test-ssl.c mysql-4.1.12.patched/vio/test-ssl.c
--- mysql-4.1.12/vio/test-ssl.c	2005-05-13 07:32:49.000000000 -0400
+++ mysql-4.1.12.patched/vio/test-ssl.c	2006-02-01 12:36:16.000000000 -0500
@@ -91,7 +91,8 @@
 
   ssl_acceptor = new_VioSSLAcceptorFd(server_key, server_cert, ca_file,
 				      ca_path, cipher);
-  ssl_connector = new_VioSSLConnectorFd(client_key, client_cert, ca_file,
+  ssl_connector = new_VioSSLConnectorFd(NULL,
+					client_key, client_cert, ca_file,
 					ca_path, cipher);
 
   client_vio = (struct st_vio*)my_malloc(sizeof(struct st_vio),MYF(0));
diff --unified --recursive mysql-4.1.12/vio/test-sslclient.c mysql-4.1.12.patched/vio/test-sslclient.c
--- mysql-4.1.12/vio/test-sslclient.c	2005-05-13 07:32:27.000000000 -0400
+++ mysql-4.1.12.patched/vio/test-sslclient.c	2006-02-01 12:36:16.000000000 -0500
@@ -61,7 +61,7 @@
 	if (ca_path!=0)
 		printf("CApath          : %s\n", ca_path);
 
-	ssl_connector = new_VioSSLConnectorFd(client_key, client_cert, ca_file, ca_path, cipher);
+	ssl_connector = new_VioSSLConnectorFd(NULL, client_key, client_cert, ca_file, ca_path, cipher);
 	if(!ssl_connector) {
                  fatal_error("client:new_VioSSLConnectorFd failed");
 	}
diff --unified --recursive mysql-4.1.12/vio/viossl.c mysql-4.1.12.patched/vio/viossl.c
--- mysql-4.1.12/vio/viossl.c	2005-05-13 07:32:44.000000000 -0400
+++ mysql-4.1.12.patched/vio/viossl.c	2006-02-01 12:36:38.000000000 -0500
@@ -1,3 +1,4 @@
+#include <netdb.h>
 /* Copyright (C) 2000 MySQL AB
 
    This program is free software; you can redistribute it and/or modify
@@ -382,10 +383,12 @@
     vio_blocking(vio, net_blocking, &unused);
     DBUG_RETURN(1);
   }  
+
+  server_cert = SSL_get_peer_certificate ((SSL*) vio->ssl_arg);
+
 #ifndef DBUG_OFF
   DBUG_PRINT("info",("SSL_get_cipher_name() = '%s'"
 		     ,SSL_get_cipher_name((SSL*) vio->ssl_arg)));
-  server_cert = SSL_get_peer_certificate ((SSL*) vio->ssl_arg);
   if (server_cert != NULL)
   {
     DBUG_PRINT("info",("Server certificate:"));
@@ -401,12 +404,72 @@
       We could do all sorts of certificate verification stuff here before
       deallocating the certificate.
     */
-    X509_free (server_cert);
   }
   else
     DBUG_PRINT("info",("Server does not have certificate."));
 #endif
+
+  if (server_cert == NULL)
+    goto failed;
+
+  if (SSL_get_verify_result(vio->ssl_arg) != X509_V_OK) 
+    goto failed;
+
+    // If we don't have a mysql struct, then we can't continue verifying. We'll
+    // assume everything is okay (generally a bad assumption).
+    if (ptr->mysql == NULL) 
+      goto success;
+
+
+  // We already know that the certificate exchanged was valid; OpenSSL
+  // handled that. Now we need to verify that the contents of the certificate
+  // are what we expect.
+
+  {
+    char *cp1, *cp2;
+    char inbuf[256];
+
+    // If we can't look up the server's hostname, or we can't verify that 
+    // hostname against the cert, then we've failed the cert check.
+
+    // We have to dig the hostname out of the options...
+    if (! ptr->mysql->host) {
+      DBUG_PRINT("error", ("SSL: no hostname, can't verify cert"));
+      goto failed;
+    }
+
+    strncpy(inbuf, 
+	    X509_NAME_oneline(X509_get_subject_name(server_cert), NULL, 0),
+	    sizeof(inbuf));
+    cp1 = strstr(inbuf, "/CN=");
+    if (cp1 == NULL) {
+      DBUG_PRINT("error", ("SSL: can't find cert CN"));
+      goto failed;
+    }
+
+    cp1 += 4; // Skip the "/CN=" that we found
+    cp2 = strchr(cp1, '/');
+    if (cp2 != NULL) *cp2 = '\0';
+    if (strcmp(cp1, ptr->mysql->host)) {
+      DBUG_PRINT("error", ("SSL: cert name '%s' doesn't match name '%s'",
+		 cp1, ptr->mysql->host));
+      goto failed;
+    }
+  }
+
+ success:
+  X509_free (server_cert);
   DBUG_RETURN(0);
+
+ failed:
+  DBUG_PRINT("error", ("SSL certificate validation failure"));
+  report_errors();
+  SSL_free((SSL*) vio->ssl_arg);
+  vio->ssl_arg = 0;
+  vio_reset(vio, old_type,vio->sd,0,FALSE);
+  vio_blocking(vio, net_blocking, &unused);
+  X509_free (server_cert);
+  DBUG_RETURN(1);
 }
 
 
diff --unified --recursive mysql-4.1.12/vio/viosslfactories.c mysql-4.1.12.patched/vio/viosslfactories.c
--- mysql-4.1.12/vio/viosslfactories.c	2005-05-13 07:32:30.000000000 -0400
+++ mysql-4.1.12.patched/vio/viosslfactories.c	2006-02-03 12:13:26.000000000 -0500
@@ -208,13 +208,15 @@
 */
 
 struct st_VioSSLConnectorFd *
-new_VioSSLConnectorFd(const char* key_file,
+new_VioSSLConnectorFd(MYSQL *mysql,
+		      const char* key_file,
 		      const char* cert_file,
 		      const char* ca_file,
 		      const char* ca_path,
 		      const char* cipher)
 {
-  int	verify = SSL_VERIFY_NONE;
+  int	verify = SSL_VERIFY_PEER;
+;
   struct st_VioSSLConnectorFd* ptr;
   int result;
   DH *dh;
@@ -229,6 +231,7 @@
 
   ptr->ssl_context= 0;
   ptr->ssl_method=  0;
+  ptr->mysql = mysql;
   /* FIXME: constants! */
 
 #ifdef __NETWARE__
[7 Feb 2006 22:21] Jorj Bauer
In case it's not clear, this is a showstopper. From a security standpoint is means SSL is unusable.
[8 Feb 2006 14:58] Hartmut Holzgraefe
Could you please attache the patch as a file to this bug report?
The word wrapping applied to the text field during transmission
corrupted it :(
[9 Feb 2006 9:53] Jorj Bauer
diff in original report

Attachment: ssl-client-cert-check-patch3.diff (text/x-patch), 6.61 KiB.

[9 Feb 2006 9:54] Jorj Bauer
added patch as a file.
[18 Feb 2006 14:33] Nick Drake
At least related, if not the cause:  in libmysqld/sql_acl.cpp  access is not denied if the issuer and/or subject of the certificate mismatch.

Starting line 853 of said file, + indicates lines need to be added.

      DBUG_PRINT("info",("checkpoint 2"));
      /* If X509 issuer is specified, we check it... */
      if (acl_user->x509_issuer)
      {
        DBUG_PRINT("info",("checkpoint 3"));
    char *ptr = X509_NAME_oneline(X509_get_issuer_name(cert), 0, 0);
    DBUG_PRINT("info",("comparing issuers: '%s' and '%s'",
               acl_user->x509_issuer, ptr));
        if (strcmp(acl_user->x509_issuer, ptr))
        {
          if (global_system_variables.log_warnings)
            sql_print_information("X509 issuer mismatch: should be '%s' "
                  "but is '%s'", acl_user->x509_issuer, ptr);
          free(ptr);
+         user_access=NO_ACCESS;
          break;
        }
        user_access= acl_user->access;
        free(ptr);
      }
      DBUG_PRINT("info",("checkpoint 4"));
      /* X509 subject is specified, we check it .. */
      if (acl_user->x509_subject)
      {
        char *ptr= X509_NAME_oneline(X509_get_subject_name(cert), 0, 0);
        DBUG_PRINT("info",("comparing subjects: '%s' and '%s'",
                           acl_user->x509_subject, ptr));
        if (strcmp(acl_user->x509_subject,ptr))
        {
          if (global_system_variables.log_warnings)
            sql_print_information("X509 subject mismatch: '%s' vs '%s'",
                            acl_user->x509_subject, ptr);
+         user_access=NO_ACCESS;
        }
        else
          user_access= acl_user->access;
        free(ptr);
      }
[3 Mar 2006 13:01] Domas Mituzas
Verified at ChangeSet@1.2214, 2006-03-03 11:16:56+01:00

Thanks for submitting these patches, MITM prevention should really exist.
[10 Mar 2006 11:36] Magnus Blåudd
Investigating how the "client" could be made more secure.
[10 Mar 2006 11:38] Magnus Blåudd
Checked the code form "sql_acl.cc" above and it starts the function with setting "user_access= NO_ACCESS" as the default. And unless it find's any valid grant's NO_ACCESS is what the function will return. 

If you are experiencing any problem, please let me know how to repeat it.
[18 Apr 2006 15:59] Bugs System
A patch for this bug has been committed. After review, it may
be pushed to the relevant source trees for release in the next
version. You can access the patch from:

  http://lists.mysql.com/commits/5091
[18 Apr 2006 16:48] Chad MILLER
This now checks the Common Name in the cert against the name of the host we intended to connect to, which is good.  I'm worried that we don't care about the authority of the certificate, as anyone can author a certificate to claim to be anything.

I'd feel better if we also had a failing test for using a cert with the same "localhost" CN/hostname, where the cert is generated by a CA we do not trust.
[19 Apr 2006 14:42] Chad MILLER
I see no problem with this patch, per se.  Magnus is going to begin to get the other half of the security mechanism in place tomorrow.
[9 May 2006 9:41] Magnus Blåudd
The tests for connecting to server with invalid certs etc:

http://lists.mysql.com/commits/5876
[10 May 2006 7:52] Magnus Blåudd
Pushed to 5.0.22
[12 May 2006 11:45] Magnus Blåudd
Pushed to 5.1.11
[12 May 2006 11:46] Magnus Blåudd
5.1 is now updated with the latest yaSSL patches.
[13 May 2006 5:05] Paul Dubois
Noted in 5.0.22, 5.1.11 changelogs.

Added the <option>--ssl-verify-server-cert</option> option to
MySQL client programs. This option causes the server's Common
Name value in its certificate to be verified against the
hostname used when connecting to the server, and the
connection is rejected if there is a mismatch. Added
<literal>OPT_SSL_VERIFY_SERVER_CERT</literal> option for the
<literal>mysql_options()</literal> C API function to enable
this verification. This feature can be used to prevent
man-in-the-middle attacks. Verification is disabled by
default.

Also updates SSL option section and description for
mysql_options().
[13 May 2006 5:13] Paul Dubois
Correction: The mysql_options() option name is
MYSQL_OPT_SSL_VERIFY_SERVER_CERT.