diff --git a/mysql_ssl_rsa_setup.cc b/mysql_ssl_rsa_setup.cc new file mode 100644 index 0000000..1ac0484 --- /dev/null +++ b/mysql_ssl_rsa_setup.cc @@ -0,0 +1,763 @@ +/* + Copyright (c) 2015, 2021, Oracle and/or its affiliates. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License, version 2.0, + as published by the Free Software Foundation. + + This program is also distributed with certain software (including + but not limited to OpenSSL) that is licensed under separate terms, + as designated in a particular file or component or in included license + documentation. The authors of MySQL hereby grant you an additional + permission to link the program and your derivative works with the + separately licensed software that they have included with MySQL. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License, version 2.0, for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +/* C Headers */ +#include +#include +#include + +/* C++ Headers */ +#include +#include +#include +#include +#include +#include +#include + +/* MySQL Headers */ +#include +#include +#include +#include +#include +#include +#include /* ORACLE_WELCOME_COPYRIGHT_NOTICE */ +#include +#include "path.h" +#include "logger.h" + +#if HAVE_CHOWN +#include +#endif +/* Utility Version */ +#define MY_VERSION "1.0.0" + +/* Forward declarations */ + +using namespace std; +typedef string Sql_string_t; + +static Sql_string_t create_string(const char *ptr); + +/* Global Variables */ +enum certs +{ + CA_CERT=0, + CA_KEY, + CA_REQ, + SERVER_CERT, + SERVER_KEY, + SERVER_REQ, + CLIENT_CERT, + CLIENT_KEY, + CLIENT_REQ, + PRIVATE_KEY, + PUBLIC_KEY, + OPENSSL_RND +}; + +enum extfiles +{ + CAV3_EXT=0, + CERTV3_EXT +}; + +Sql_string_t cert_files[] = +{ + create_string("ca.pem"), + create_string("ca-key.pem"), + create_string("ca-req.pem"), + create_string("server-cert.pem"), + create_string("server-key.pem"), + create_string("server-req.pem"), + create_string("client-cert.pem"), + create_string("client-key.pem"), + create_string("client-req.pem"), + create_string("private_key.pem"), + create_string("public_key.pem"), + create_string(".rnd") +}; + +Sql_string_t ext_files[] = +{ + create_string("cav3.ext"), + create_string("certv3.ext") +}; + +#define MAX_PATH_LEN (FN_REFLEN - strlen(FN_DIRSEP) \ + - cert_files[SERVER_CERT].length() - 1) +/* + Higest number of fixed characters in subject line is 47: + MySQL_SERVER__Auto_Generated_Server_Certificate + Maximum size of subject is 64. So suffix can't be longer + than 17 characters. +*/ +#define MAX_SUFFIX_LEN 17 + +Log info(cout,"NOTE"); +Log error(cerr,"ERROR"); + +char **defaults_argv= 0; +static char *opt_datadir= 0; +static char default_data_dir[]= MYSQL_DATADIR; +static char *opt_suffix= 0; +static char default_suffix[]= MYSQL_SERVER_VERSION; +#if HAVE_CHOWN +static char *opt_userid= 0; +struct passwd *user_info= 0; +#endif /* HAVE_CHOWN */ +Path dir_string; +Sql_string_t suffix_string; +my_bool opt_verbose; + +static const char *load_default_groups[]= +{ + "mysql_ssl_rsa_setup", + "mysql_install_db", + "mysqld", + 0 +}; + +static struct my_option my_options[]= { + {"help", '?', "Display this help and exit.", 0, 0, 0, GET_NO_ARG, + NO_ARG, 0, 0, 0, 0, 0, 0}, + {"verbose", 'v', "Be more verbose when running program", + &opt_verbose, 0, 0, GET_BOOL, NO_ARG, FALSE, 0, 0, 0, 0, 0}, + {"version", 'V', "Print program version and exit", 0, 0, 0, GET_NO_ARG, + NO_ARG, 0, 0, 0, 0, 0, 0}, + {"datadir", 'd', "Directory to store generated files.", &opt_datadir, &opt_datadir, 0, + GET_STR_ALLOC, REQUIRED_ARG, (longlong)&default_data_dir, 0, 0, 0, 0, 0}, + {"suffix", 's', + "Suffix to be added in certificate subject line", &opt_suffix, + &opt_suffix, 0, GET_STR_ALLOC, REQUIRED_ARG, (longlong) &default_suffix, + 0, 0, 0, 0, 0}, +#if HAVE_CHOWN + {"uid", 0, "The effective user id to be used for file permission", + &opt_userid, &opt_userid, 0, GET_STR_ALLOC, REQUIRED_ARG, 0, 0, 0, + 0, 0, 0}, +#endif /* HAVE_CHOWN */ + /* END TOKEN */ + {0, 0, 0, 0, 0, 0, GET_NO_ARG, NO_ARG, 0, 0, 0, 0, 0, 0} +}; + +/* Helper Functions */ + +/** + The string class will break if constructed with a NULL pointer. This wrapper + provides a systematic protection when importing char pointers. + */ +static +Sql_string_t create_string(const char *ptr) +{ + return (ptr ? Sql_string_t(ptr) : Sql_string_t("")); +} + + +static +int execute_command(const Sql_string_t &command, + const Sql_string_t &error_message) +{ + stringstream cmd_string; + + cmd_string << command; + if (!opt_verbose) + { +#ifndef _WIN32 + cmd_string << " > /dev/null 2>&1"; +#else + cmd_string << " > NUL 2>&1"; +#endif /* _WIN32 */ + } + + info << "Executing : " << cmd_string.str() << endl; + if (system(cmd_string.str().c_str())) + { + error << error_message << endl; + return 1; + } + + return 0; +} + + +static +int set_file_pair_permission(const Sql_string_t &priv, + const Sql_string_t &pub) +{ + if (MY_TEST(my_chmod(priv.c_str(), + USER_READ| USER_WRITE, MYF(MY_FAE+MY_WME))) || + MY_TEST(my_chmod(pub.c_str(), + USER_READ|USER_WRITE|GROUP_READ|OTHERS_READ, + MYF(MY_FAE+MY_WME)))) + { + error << "Error setting file permissions for" << priv.c_str() + << " and " << pub.c_str() << endl; + return 1; + } +#if HAVE_CHOWN + if (user_info) + { + if(chown(priv.c_str(), user_info->pw_uid, user_info->pw_gid) || + chown(pub.c_str(), user_info->pw_uid, user_info->pw_gid)) + { + error << "Failed to change file permission" << endl; + return 1; + } + } +#endif /* HAVE_CHOWN */ + return 0; +} + + +static +bool file_exists(const Sql_string_t &filename) +{ + MY_STAT file_stat; + if (my_stat(filename.c_str(), &file_stat, MYF(0)) == NULL) + return false; + + return true; +} + + +static +int remove_file(const Sql_string_t &filename, bool report_error=true) +{ + if (my_delete(filename.c_str(), MYF(0))) + { + if (report_error) + error << "Error deleting : " << filename << endl; + return 1; + } + return 0; +} + + +static +void free_resources() +{ + if (opt_datadir) + my_free(opt_datadir); + if (opt_suffix) + my_free(opt_suffix); +#if HAVE_CHOWN + if (opt_userid) + my_free(opt_userid); +#endif + if (defaults_argv && *defaults_argv) + free_defaults(defaults_argv); +} + + +class RSA_priv +{ +public: + RSA_priv(uint32_t key_size= 2048) + : m_key_size(key_size) {}; + + ~RSA_priv() {}; + + Sql_string_t operator()(const Sql_string_t &key_file) + { + stringstream command; + command << "openssl genrsa " << " -out " << key_file << " " << m_key_size; + + return command.str(); + } +private: + uint32_t m_key_size; +}; + + +class RSA_pub +{ +public: + Sql_string_t operator()(const Sql_string_t &priv_key_file, + const Sql_string_t &pub_key_file) + { + stringstream command; + command << "openssl rsa -in " << priv_key_file << " -pubout -out " + << pub_key_file; + return command.str(); + } +}; + + +class X509_key +{ +public: + X509_key(const Sql_string_t &version, + uint32_t validity= 10*365L) + : m_validity(validity) + { + m_subj_prefix << "-subj /CN=MySQL_Server_" << version; + } + + Sql_string_t operator()(Sql_string_t suffix, + const Sql_string_t &key_file, + const Sql_string_t &req_file) + { + stringstream command; + command << "openssl req -newkey rsa:2048 -days " << m_validity + << " -nodes -keyout " << key_file << " " + << m_subj_prefix.str() << suffix << " -out " << req_file + << " && openssl rsa -in " << key_file << " -out " << key_file; + + return command.str(); + } + +private: + uint32_t m_validity; + stringstream m_subj_prefix; +}; + +class X509v3_ext_writer +{ +public: + X509v3_ext_writer() + { + m_cav3_ext_options << "basicConstraints=CA:TRUE" << std::endl; + + m_certv3_ext_options << "basicConstraints=CA:FALSE" << std::endl; + } + ~X509v3_ext_writer() {}; + + bool operator()(const Sql_string_t &cav3_ext_file, + const Sql_string_t &certv3_ext_file) + { + if (!cav3_ext_file.length() || + !certv3_ext_file.length()) + return true; + + std::ofstream ext_file; + + ext_file.open(cav3_ext_file.c_str(), + std::ios::out|std::ios::trunc); + if (!ext_file.is_open()) + return true; + ext_file << m_cav3_ext_options.str(); + ext_file.close(); + + ext_file.open(certv3_ext_file.c_str(), + std::ios::out|std::ios::trunc); + if (!ext_file.is_open()) + { + remove_file(cav3_ext_file.c_str(), false); + return true; + } + ext_file << m_certv3_ext_options.str(); + ext_file.close(); + + return false; + } +private: + stringstream m_cav3_ext_options; + stringstream m_certv3_ext_options; +}; + +class X509_cert +{ +public: + X509_cert(uint32_t validity= 10*365L) + : m_validity(validity) {}; + + ~X509_cert() {}; + + Sql_string_t operator()(const Sql_string_t &req_file, + const Sql_string_t &cert_file, + uint32_t serial, + bool self_signed, + const Sql_string_t &sign_key_file, + const Sql_string_t &sign_cert_file, + const Sql_string_t &ext_file) + { + stringstream command; + command << "openssl x509 -sha256 -days " << m_validity; + command << " -extfile " << ext_file; + command << " -set_serial " << serial << " -req -in " << req_file; + if (self_signed) + command << " -signkey " << sign_key_file; + else + command << " -CA " << sign_cert_file << " -CAkey " << sign_key_file; + command << " -out " << cert_file; + + return command.str(); + } + +protected: + uint32_t m_validity; +}; + + +static +void print_version(void) +{ + cout << my_progname << " Version : " << MY_VERSION + << " Distribution : " << MYSQL_SERVER_VERSION + << " For : " << SYSTEM_TYPE << " On : " << MACHINE_TYPE << endl; +} + + +static +void usage(void) +{ + print_version(); + cout << (ORACLE_WELCOME_COPYRIGHT_NOTICE(COPYRIGHT_NOTICE_STONEDB_BEGIN_YEAR)) << endl + << "MySQL SSL Certificate and RSA Key Generation Utility" << endl + << "Usage : " << my_progname << " [OPTIONS]" << endl; + + my_print_help(my_options); + my_print_variables(my_options); +} + + +my_bool +my_arguments_get_one_option(int optid, + const struct my_option *opt MY_ATTRIBUTE((unused)), + char *argument) +{ + switch(optid){ + case '?': + usage(); + free_resources(); + exit(0); + case 'V': + print_version(); + free_resources(); + exit(0); + } + return 0; +} + + +static inline +bool is_not_alnum_underscore(char c) +{ + return !(isalnum(c) || c == '_'); +} + +static +bool check_suffix() +{ + return (strcmp(opt_suffix, default_suffix) && + (find_if(suffix_string.begin(), suffix_string.end(), + is_not_alnum_underscore) != suffix_string.end())); +} + + +int main(int argc, char *argv[]) +{ + int ret_val= 0; + Sql_string_t openssl_check("openssl version"); + my_bool save_skip_unknown= my_getopt_skip_unknown; + + MY_INIT(argv[0]); + DBUG_ENTER("main"); + DBUG_PROCESS(argv[0]); + + /* Parse options : Command Line/Config file */ + +#ifdef _WIN32 + /* Convert command line parameters from UTF16LE to UTF8MB4. */ + my_win_translate_command_line_args(&my_charset_utf8mb4_bin, &argc, &argv); +#endif + my_getopt_use_args_separator= TRUE; + if (load_defaults("my", load_default_groups, &argc, &argv)) + { + my_end(0); + free_resources(); + exit(1); + } + + MY_MODE file_creation_mode= get_file_perm(USER_READ | USER_WRITE); + MY_MODE saved_umask= umask(~(file_creation_mode)); + + defaults_argv= argv; + my_getopt_use_args_separator= FALSE; + my_getopt_skip_unknown= TRUE; + + if (handle_options(&argc, &argv, my_options, + my_arguments_get_one_option)) + { + error << "Error parsing options" << endl; + ret_val= 1; + goto end; + } + + my_getopt_skip_unknown= save_skip_unknown; + + /* Process opt_verbose */ + if (opt_verbose != TRUE) + info.enabled(false); + + /* Process opt_datadir */ + + dir_string.path(create_string(opt_datadir)); + if (dir_string.to_str().length() > MAX_PATH_LEN) + { + error << "Dir path is too long" << endl; + ret_val= 1; + goto end; + } + + if (!dir_string.normalize_path() || !dir_string.exists()) + { + error << "Failed to access directory pointed by --datadir. " + << "Please make sure that directory exists and is " + << "accessible by mysql_ssl_rsa_setup. Supplied value : " + << dir_string.to_str() << endl; + ret_val= 1; + goto end; + } + + info << "Destination directory: " << dir_string.to_str() << endl; + + /* Process opt_suffix */ + + suffix_string.append(opt_suffix); + if (suffix_string.length() > MAX_SUFFIX_LEN) + { + error << "Maximum number of characters allowed as the value for " + << "--suffix are " << MAX_SUFFIX_LEN << endl; + ret_val= 1; + goto end; + } + + if (check_suffix()) + { + error << "Invalid string for --suffix option. Either use default value for " + << "the option or provide a string with alphanumeric characters " + << "and/or _ only." << endl; + ret_val= 1; + goto end; + } + + if ((ret_val= execute_command(openssl_check, "Could not find OpenSSL on the system"))) + { + goto end; + } + else + { + char save_wd[FN_REFLEN]; + bool files_exist= false; + Sql_string_t verify("openssl verify -CAfile "); + + if (my_getwd(save_wd, FN_REFLEN-1, MYF(MY_WME))) + { + error << "Error saving current working directory" << endl; + ret_val= 1; + goto end; + } + + if (my_setwd(dir_string.to_str().c_str(), MYF(MY_WME))) + { + error << "Error changing working directory" << endl; + ret_val= 1; + goto end; + } +#if HAVE_CHOWN + if (opt_userid && geteuid() == 0) + { + user_info= getpwnam(opt_userid); + if (!user_info) + { + error << "Error fetching user information" << endl; + ret_val= 1; + goto end; + } + } +#endif /* HAVE_CHOWN */ + + /* + SSL Certificate Generation. + 1. Check for ca.pem, server_cert.pem and server_key.pem at + the directory location provided by --dir. + 2. If none of these files are present, generate following + files: + ca.pem, ca_key.pem + server_cert.pem, server_key.pem + client_cert.pem, client_key.pem + 3. If everything goes smoothly, set permission on files. + */ + + files_exist= file_exists(cert_files[CA_CERT]) || + file_exists(cert_files[SERVER_CERT]) || + file_exists(cert_files[SERVER_KEY]); + + if (files_exist) + { + info << "Certificate files are present in given dir. Skipping generation." << endl; + } + else + { + Sql_string_t empty_string(""); + X509_key x509_key(suffix_string); + X509_cert x509_cert; + X509v3_ext_writer x509v3_ext_writer; + + /* Delete existing files if any */ + remove_file(cert_files[CA_REQ], false); + remove_file(cert_files[SERVER_REQ], false); + remove_file(cert_files[CLIENT_REQ], false); + remove_file(cert_files[CLIENT_CERT], false); + remove_file(cert_files[CLIENT_KEY], false); + remove_file(cert_files[OPENSSL_RND], false); + + /* Remove existing v3 extension files */ + remove_file(ext_files[CAV3_EXT], false); + remove_file(ext_files[CERTV3_EXT], false); + + /* Create v3 extension files */ + if (x509v3_ext_writer(ext_files[CAV3_EXT], ext_files[CERTV3_EXT])) + goto end; + + /* Generate CA Key and Certificate */ + if ((ret_val= execute_command(x509_key("_Auto_Generated_CA_Certificate", + cert_files[CA_KEY], cert_files[CA_REQ]), + "Error generating ca_key.pem and ca_req.pem"))) + goto end; + + if ((ret_val= execute_command(x509_cert(cert_files[CA_REQ], cert_files[CA_CERT], 1, + true, cert_files[CA_KEY], empty_string, + ext_files[CAV3_EXT]), + "Error generating ca_cert.pem"))) + goto end; + + /* Generate Server Key and Certificate */ + if ((ret_val= execute_command(x509_key("_Auto_Generated_Server_Certificate", + cert_files[SERVER_KEY], cert_files[SERVER_REQ]), + "Error generating server_key.pem and server_req.pem"))) + goto end; + + if ((ret_val= execute_command(x509_cert(cert_files[SERVER_REQ], cert_files[SERVER_CERT], 2, + false, cert_files[CA_KEY], cert_files[CA_CERT], + ext_files[CERTV3_EXT]), + "Error generating server_cert.pem"))) + goto end; + + /* Generate Client Key and Certificate */ + if ((ret_val= execute_command(x509_key("_Auto_Generated_Client_Certificate", + cert_files[CLIENT_KEY], cert_files[CLIENT_REQ]), + "Error generating client_key.pem and client_req.pem"))) + goto end; + + if ((ret_val= execute_command(x509_cert(cert_files[CLIENT_REQ], cert_files[CLIENT_CERT], 3, + false, cert_files[CA_KEY], cert_files[CA_CERT], + ext_files[CERTV3_EXT]), + "Error generating client_cert.pem"))) + goto end; + + /* Verify generated certificates */ + verify.append(cert_files[CA_CERT]); + verify.append(" "); + verify.append(cert_files[SERVER_CERT]); + verify.append(" "); + verify.append(cert_files[CLIENT_CERT]); + if ((ret_val= execute_command(verify, "Verification of X509 certificates failed."))) + goto end; + + /* Set permission */ + if ((ret_val= (set_file_pair_permission(cert_files[CA_KEY], + cert_files[CA_CERT]) | + set_file_pair_permission(cert_files[SERVER_KEY], + cert_files[SERVER_CERT]) | + set_file_pair_permission(cert_files[CLIENT_KEY], + cert_files[CLIENT_CERT])))) + goto end; + + /* Remove request files : Flag an error if we can't delete them. */ + if ((ret_val= remove_file(cert_files[CA_REQ]))) + goto end; + + if ((ret_val= remove_file(cert_files[SERVER_REQ]))) + goto end; + + if ((ret_val= remove_file(cert_files[CLIENT_REQ]))) + goto end; + + remove_file(cert_files[OPENSSL_RND], false); + + /* Remove existing v3 extension files */ + remove_file(ext_files[CAV3_EXT], false); + remove_file(ext_files[CERTV3_EXT], false); + + } + + /* + RSA Key pair generation. + 1. Check if private_key.pem or public_key.pem are present at + the directory location provided by --dir. + 2. If not, generate private_key.pem, public_key.pem and + set permission after successful generation. + */ + + files_exist= file_exists(cert_files[PRIVATE_KEY]) || + file_exists(cert_files[PUBLIC_KEY]); + + if (files_exist) + { + info << "RSA key files are present in given dir. Skipping generation." << endl; + } + else + { + RSA_priv rsa_priv; + RSA_pub rsa_pub; + + /* Remove existing file if any */ + remove_file(cert_files[OPENSSL_RND], false); + + if ((ret_val= execute_command(rsa_priv(cert_files[PRIVATE_KEY]), + "Error generating private_key.pem"))) + goto end; + + if ((ret_val= execute_command(rsa_pub(cert_files[PRIVATE_KEY], + cert_files[PUBLIC_KEY]), + "Error generating public_key.pem"))) + goto end; + /* Set Permission */ + if ((ret_val= set_file_pair_permission(cert_files[PRIVATE_KEY], + cert_files[PUBLIC_KEY]))) + goto end; + + remove_file(cert_files[OPENSSL_RND], false); + } + + if (my_setwd(save_wd, MYF(MY_WME))) + { + error << "Error changing working directory" << endl; + ret_val= 1; + goto end; + } + } + + info << "Success!" << endl; + +end: + + umask(saved_umask); + free_resources(); + + DBUG_RETURN(ret_val); +}