diff --git a/include/ma_tls.h b/include/ma_tls.h index 616124c0..6601f896 100644 --- a/include/ma_tls.h +++ b/include/ma_tls.h @@ -27,6 +27,7 @@ typedef struct st_ma_pvio_tls { void *data; MARIADB_PVIO *pvio; void *ssl; + MARIADB_X509_INFO cert_info; } MARIADB_TLS; /* Function prototypes */ diff --git a/include/mysql.h b/include/mysql.h index 093f19e0..1d30486a 100644 --- a/include/mysql.h +++ b/include/mysql.h @@ -33,6 +33,7 @@ extern "C" { #endif #include +#include #if !defined (_global_h) && !defined (MY_GLOBAL_INCLUDED) /* If not standard header */ #include @@ -297,7 +298,8 @@ extern const char *SQLSTATE_UNKNOWN; MARIADB_CONNECTION_EXTENDED_SERVER_CAPABILITIES, MARIADB_CONNECTION_CLIENT_CAPABILITIES, MARIADB_CONNECTION_BYTES_READ, - MARIADB_CONNECTION_BYTES_SENT + MARIADB_CONNECTION_BYTES_SENT, + MARIADB_TLS_PEER_CERT_INFO, }; enum mysql_status { MYSQL_STATUS_READY, @@ -481,6 +483,27 @@ struct st_mysql_client_plugin MYSQL_CLIENT_PLUGIN_HEADER }; +enum mariadb_tls_verification { + MARIADB_VERIFY_NONE = 0, + MARIADB_VERIFY_PIPE, + MARIADB_VERIFY_UNIXSOCKET, + MARIADB_VERIFY_LOCALHOST, + MARIADB_VERIFY_FINGERPRINT, + MARIADB_VERIFY_PEER_CERT +}; + +typedef struct +{ + int version; + char *issuer; + char *subject; + char fingerprint[65]; + struct tm not_before; + struct tm not_after; + enum mariadb_tls_verification verify_mode; +} MARIADB_X509_INFO; + + struct st_mysql_client_plugin * mysql_load_plugin(struct st_mysql *mysql, const char *name, int type, int argc, ...); diff --git a/libmariadb/ma_pvio.c b/libmariadb/ma_pvio.c index 6f726dbd..95d02d57 100644 --- a/libmariadb/ma_pvio.c +++ b/libmariadb/ma_pvio.c @@ -549,13 +549,24 @@ static my_bool ignore_self_signed_cert_error(MARIADB_PVIO *pvio) "127.0.0.1", "::1", NULL}; int i; if (pvio->type != PVIO_TYPE_SOCKET) + { + pvio->ctls->cert_info.verify_mode= +#ifdef WIN32 + MARIADB_VERIFY_PIPE; +#else + MARIADB_VERIFY_UNIXSOCKET; +#endif return TRUE; + } if (!hostname) return FALSE; for (i= 0; local_host_names[i]; i++) { if (strcmp(hostname, local_host_names[i]) == 0) + { + pvio->ctls->cert_info.verify_mode= MARIADB_VERIFY_LOCALHOST; return TRUE; + } } return FALSE; } @@ -586,6 +597,8 @@ my_bool ma_pvio_start_ssl(MARIADB_PVIO *pvio) !pvio->mysql->net.tls_self_signed_error && ma_pvio_tls_verify_server_cert(pvio->ctls)) return 1; + else + pvio->ctls->cert_info.verify_mode= MARIADB_VERIFY_PEER_CERT; if (pvio->mysql->options.extension && ((pvio->mysql->options.extension->tls_fp && pvio->mysql->options.extension->tls_fp[0]) || @@ -595,6 +608,7 @@ my_bool ma_pvio_start_ssl(MARIADB_PVIO *pvio) pvio->mysql->options.extension->tls_fp, pvio->mysql->options.extension->tls_fp_list)) return 1; + pvio->ctls->cert_info.verify_mode= MARIADB_VERIFY_FINGERPRINT; reset_tls_self_signed_error(pvio->mysql); // validated } diff --git a/libmariadb/mariadb_lib.c b/libmariadb/mariadb_lib.c index 31a062e6..52f96889 100644 --- a/libmariadb/mariadb_lib.c +++ b/libmariadb/mariadb_lib.c @@ -4519,6 +4519,11 @@ my_bool mariadb_get_infov(MYSQL *mysql, enum mariadb_value value, void *arg, ... va_start(ap, arg); switch(value) { +#ifdef HAVE_TLS + case MARIADB_TLS_PEER_CERT_INFO: + *((MARIADB_X509_INFO **)arg)= mysql->net.pvio->ctls ? (MARIADB_X509_INFO *)&mysql->net.pvio->ctls->cert_info : NULL; + break; +#endif case MARIADB_MAX_ALLOWED_PACKET: *((size_t *)arg)= (size_t)max_allowed_packet; break; diff --git a/libmariadb/secure/gnutls.c b/libmariadb/secure/gnutls.c index 9a83a682..7b039f35 100644 --- a/libmariadb/secure/gnutls.c +++ b/libmariadb/secure/gnutls.c @@ -1173,6 +1173,8 @@ my_bool ma_tls_connect(MARIADB_TLS *ctls) MYSQL *mysql= (MYSQL *)gnutls_session_get_ptr(ssl); MARIADB_PVIO *pvio; int ret; + const gnutls_datum_t *cert_list; + unsigned int list_size= 0; if (!mysql) return 1; @@ -1214,6 +1216,39 @@ my_bool ma_tls_connect(MARIADB_TLS *ctls) return 1; } ctls->ssl= (void *)ssl; + + /* retrieve peer certificate information */ + if ((cert_list= gnutls_certificate_get_peers(ssl, &list_size))) + { + gnutls_x509_crt_t cert; + + gnutls_x509_crt_init(&cert); + memset(&ctls->cert_info, 0, sizeof(MARIADB_X509_INFO)); + + if (!gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER)) + { + size_t len= 0; + time_t notBefore, notAfter; + + ctls->cert_info.version= gnutls_x509_crt_get_version(cert); + + gnutls_x509_crt_get_issuer_dn(cert, NULL, &len); + if ((ctls->cert_info.issuer= (char *)malloc(len))) + gnutls_x509_crt_get_issuer_dn(cert, ctls->cert_info.issuer, &len); + + gnutls_x509_crt_get_dn(cert, NULL, &len); + if ((ctls->cert_info.subject= (char *)malloc(len))) + gnutls_x509_crt_get_dn(cert, ctls->cert_info.subject, &len); + + notBefore= gnutls_x509_crt_get_activation_time(cert); + memcpy(&ctls->cert_info.not_before, gmtime(¬Before), sizeof(struct tm)); + + notAfter= gnutls_x509_crt_get_expiration_time(cert); + memcpy(&ctls->cert_info.not_after, gmtime(¬After), sizeof(struct tm)); + + ma_tls_get_finger_print(ctls, MA_HASH_SHA256, (char *)&ctls->cert_info.fingerprint, 32); + } + } return 0; } @@ -1321,6 +1356,8 @@ my_bool ma_tls_close(MARIADB_TLS *ctls) gnutls_certificate_free_ca_names(ctx); gnutls_certificate_free_credentials(ctx); gnutls_deinit((gnutls_session_t )ctls->ssl); + free(ctls->cert_info.issuer); + free(ctls->cert_info.subject); ctls->ssl= NULL; } return 0; diff --git a/libmariadb/secure/openssl.c b/libmariadb/secure/openssl.c index 2347d90d..8ec81872 100644 --- a/libmariadb/secure/openssl.c +++ b/libmariadb/secure/openssl.c @@ -463,6 +463,7 @@ my_bool ma_tls_connect(MARIADB_TLS *ctls) MYSQL *mysql; MARIADB_PVIO *pvio; int rc; + X509 *cert; #ifdef OPENSSL_USE_BIOMETHOD BIO_METHOD *bio_method= NULL; BIO *bio; @@ -515,7 +516,7 @@ my_bool ma_tls_connect(MARIADB_TLS *ctls) mysql->net.tls_self_signed_error= X509_verify_cert_error_string(x509_err); else if (x509_err != X509_V_OK) { - my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, + my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, ER(CR_SSL_CONNECTION_ERROR), X509_verify_cert_error_string(x509_err)); /* restore blocking mode */ if (!blocking) @@ -529,6 +530,23 @@ my_bool ma_tls_connect(MARIADB_TLS *ctls) } pvio->ctls->ssl= ctls->ssl= (void *)ssl; + /* Store peer certificate information */ + if ((cert= SSL_get_peer_certificate(ssl))) + { + char fp[33]; + const ASN1_TIME *not_before= X509_get0_notBefore(cert), + *not_after= X509_get0_notAfter(cert); + ctls->cert_info.subject= X509_NAME_oneline(X509_get_subject_name(cert), NULL, 0); + ctls->cert_info.issuer= X509_NAME_oneline(X509_get_issuer_name(cert), NULL, 0); + ctls->cert_info.version= X509_get_version(cert) + 1; + + ASN1_TIME_to_tm(not_before, (struct tm *)&ctls->cert_info.not_before); + ASN1_TIME_to_tm(not_after, (struct tm *)&ctls->cert_info.not_after); + + ma_tls_get_finger_print(ctls, MA_HASH_SHA256, fp, 33); + mysql_hex_string(ctls->cert_info.fingerprint, fp, 32); + } + return 0; } @@ -654,6 +672,9 @@ my_bool ma_tls_close(MARIADB_TLS *ctls) SSL_free(ssl); ctls->ssl= NULL; + OPENSSL_free(ctls->cert_info.issuer); + OPENSSL_free(ctls->cert_info.subject); + return rc; } @@ -739,33 +760,30 @@ unsigned int ma_tls_get_finger_print(MARIADB_TLS *ctls, uint hash_type, char *fp MYSQL *mysql; unsigned int fp_len; const EVP_MD *hash_alg; + unsigned int max_len= EVP_MAX_MD_SIZE; if (!ctls || !ctls->ssl) return 0; mysql = SSL_get_app_data(ctls->ssl); - if (len < EVP_MAX_MD_SIZE) - { - my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, - ER(CR_SSL_CONNECTION_ERROR), - "Finger print buffer too small"); - return 0; - } - switch (hash_type) { case MA_HASH_SHA1: hash_alg = EVP_sha1(); + max_len= 20; break; case MA_HASH_SHA224: hash_alg = EVP_sha224(); + max_len= 28; break; case MA_HASH_SHA256: hash_alg = EVP_sha256(); + max_len= 32; break; case MA_HASH_SHA384: hash_alg = EVP_sha384(); + max_len= 48; break; case MA_HASH_SHA512: hash_alg = EVP_sha512(); @@ -776,6 +794,14 @@ unsigned int ma_tls_get_finger_print(MARIADB_TLS *ctls, uint hash_type, char *fp "Cannot detect hash algorithm for fingerprint verification"); return 0; } + + if (len < max_len) + { + my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, + ER(CR_SSL_CONNECTION_ERROR), + "Finger print buffer too small"); + return 0; + } if (!(cert= SSL_get_peer_certificate(ctls->ssl))) { diff --git a/libmariadb/secure/schannel.c b/libmariadb/secure/schannel.c index a0f94903..a6617d2e 100644 --- a/libmariadb/secure/schannel.c +++ b/libmariadb/secure/schannel.c @@ -32,6 +32,8 @@ char tls_library_version[] = "Schannel"; #define PROT_TLS1_2 4 #define PROT_TLS1_3 8 +unsigned int ma_set_tls_x509_info(MARIADB_TLS *ctls); + static struct { DWORD cipher_id; @@ -457,6 +459,7 @@ my_bool ma_tls_connect(MARIADB_TLS *ctls) goto end; } + ma_set_tls_x509_info(ctls); rc = 0; end: @@ -511,6 +514,8 @@ my_bool ma_tls_close(MARIADB_TLS *ctls) DeleteSecurityContext(&sctx->hCtxt); } LocalFree(sctx); + LocalFree(ctls->cert_info.issuer); + LocalFree(ctls->cert_info.subject); return 0; } /* }}} */ @@ -551,6 +556,60 @@ const char *ma_tls_get_cipher(MARIADB_TLS *ctls) return cipher_name(&CipherInfo); } +unsigned char *ma_cert_blob_to_str(PCERT_NAME_BLOB cnblob) +{ + DWORD type= CERT_X500_NAME_STR; + DWORD size= CertNameToStrA(X509_ASN_ENCODING, cnblob, type, NULL, 0); + char *str= NULL; + + if (!size) + return NULL; + + str= (char *)LocalAlloc(LMEM_ZEROINIT,size); + CertNameToStrA(X509_ASN_ENCODING, cnblob, type, str, size); + return str; +} + +static void ma_systime_to_tm(SYSTEMTIME sys_tm, struct tm *tm) +{ + memset(tm, 0, sizeof(struct tm)); + tm->tm_year= sys_tm.wYear - 1900; + tm->tm_mon= sys_tm.wMonth - 1; + tm->tm_mday= sys_tm.wDay; + tm->tm_hour = sys_tm.wHour; + tm->tm_min = sys_tm.wMinute; +} + +unsigned int ma_set_tls_x509_info(MARIADB_TLS *ctls) +{ + PCCERT_CONTEXT pCertCtx= NULL; + SC_CTX *sctx= (SC_CTX *)ctls->ssl; + PCERT_INFO pci= NULL; + DWORD sizeĀ“= 0; + SYSTEMTIME tm; + char fp[33]; + + if (QueryContextAttributes(&sctx->hCtxt, SECPKG_ATTR_REMOTE_CERT_CONTEXT, (PVOID)&pCertCtx) != SEC_E_OK) + return 1; + + pci= pCertCtx->pCertInfo; + + ctls->cert_info.version= pci->dwVersion; + ctls->cert_info.subject = ma_cert_blob_to_str(&pci->Subject); + ctls->cert_info.issuer = ma_cert_blob_to_str(&pci->Issuer); + + FileTimeToSystemTime(&pci->NotBefore, &tm); + ma_systime_to_tm(tm, &ctls->cert_info.not_before); + FileTimeToSystemTime(&pci->NotAfter, &tm); + ma_systime_to_tm(tm, &ctls->cert_info.not_after); + + ma_tls_get_finger_print(ctls, MA_HASH_SHA256, fp, 33); + mysql_hex_string(ctls->cert_info.fingerprint, fp, 32); + + return 0; +} + + unsigned int ma_tls_get_finger_print(MARIADB_TLS *ctls, uint hash_type, char *fp, unsigned int len) { MA_HASH_CTX* hash_ctx; diff --git a/unittest/libmariadb/connection.c b/unittest/libmariadb/connection.c index 761e6394..6c8ca17b 100644 --- a/unittest/libmariadb/connection.c +++ b/unittest/libmariadb/connection.c @@ -1369,6 +1369,7 @@ static int test_conc276(MYSQL *unused __attribute__((unused))) MYSQL *mysql= mysql_init(NULL); int rc; my_bool val= 1; + MARIADB_X509_INFO *info; mysql_options(mysql, MYSQL_OPT_SSL_ENFORCE, &val); mysql_options(mysql, MYSQL_OPT_RECONNECT, &val); @@ -1380,7 +1381,15 @@ static int test_conc276(MYSQL *unused __attribute__((unused))) return FAIL; } diag("Cipher in use: %s", mysql_get_ssl_cipher(mysql)); + mariadb_get_infov(mysql, MARIADB_TLS_PEER_CERT_INFO, &info); + diag("subject: %s", info->subject); + diag("issuer: %s", info->issuer); + diag("fingerprint: %s", info->fingerprint); + diag("not before : %04d.%02d.%02d", info->not_before.tm_year + 1900, + info->not_before.tm_mon + 1, info->not_before.tm_mday); + diag("not after : %04d.%02d.%02d", info->not_after.tm_year + 1900, + info->not_after.tm_mon + 1, info->not_after.tm_mday); rc= mariadb_reconnect(mysql); check_mysql_rc(rc, mysql); @@ -2309,7 +2318,49 @@ static int test_conc632(MYSQL *my __attribute__((unused))) return OK; } +static int test_x509(MYSQL *my __attribute__((unused))) +{ + MYSQL *mysql1, *mysql2; + my_bool val= 1; + my_bool verify= 0; + char fp[65]; + MARIADB_X509_INFO *info; + + mysql1= mysql_init(NULL); + mysql2= mysql_init(NULL); + + mysql_options(mysql1, MYSQL_OPT_SSL_ENFORCE, &val); + mysql_options(mysql2, MYSQL_OPT_SSL_ENFORCE, &val); + + mysql_options(mysql1, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, &verify); + if (!(my_test_connect(mysql1, hostname, username, + password, schema, port, + socketname, 0))) + { + diag("connection failed"); + return FAIL; + } + mariadb_get_infov(mysql1, MARIADB_TLS_PEER_CERT_INFO, &info); + memset(fp, 0, 65); + diag("fingerprint: %s", info->fingerprint); + mysql_options(mysql2, MARIADB_OPT_TLS_PEER_FP, info->fingerprint); + if (!(my_test_connect(mysql2, hostname, username, + password, schema, port, + socketname, 0))) + { + diag("connection failed"); + return FAIL; + } + mariadb_get_infov(mysql2, MARIADB_TLS_PEER_CERT_INFO, &info); + FAIL_IF(info->verify_mode != MARIADB_VERIFY_FINGERPRINT, "Fingerprint verification expected"); + + mysql_close(mysql1); + mysql_close(mysql2); + return OK; +} + struct my_tests_st my_tests[] = { + {"test_x509", test_x509, TEST_CONNECTION_NONE, 0, NULL, NULL}, {"test_conc632", test_conc632, TEST_CONNECTION_NONE, 0, NULL, NULL}, {"test_status_callback", test_status_callback, TEST_CONNECTION_NONE, 0, NULL, NULL}, {"test_conc365", test_conc365, TEST_CONNECTION_NONE, 0, NULL, NULL},