From 70bbd2536b0eb005458f7dee419eef503ff9e0cb Mon Sep 17 00:00:00 2001 From: Jeffrey Ryan Date: Thu, 19 May 2022 15:27:30 -0500 Subject: [PATCH] core_rpc_server: new file: rpc_ssl.fingerprint --- contrib/epee/include/net/net_ssl.h | 27 +++++++++++ contrib/epee/src/net_ssl.cpp | 73 ++++++++++++++++++++++++++++++ src/gen_ssl_cert/gen_ssl_cert.cpp | 25 +--------- src/rpc/core_rpc_server.cpp | 29 +++++++++++- 4 files changed, 129 insertions(+), 25 deletions(-) diff --git a/contrib/epee/include/net/net_ssl.h b/contrib/epee/include/net/net_ssl.h index c79a3acc1..c6ef925ba 100644 --- a/contrib/epee/include/net/net_ssl.h +++ b/contrib/epee/include/net/net_ssl.h @@ -151,6 +151,33 @@ namespace net_utils bool create_ec_ssl_certificate(EVP_PKEY *&pkey, X509 *&cert); bool create_rsa_ssl_certificate(EVP_PKEY *&pkey, X509 *&cert); + /** + * @brief Create a human-readable X509 certificate fingerprint + * + * Example output: "12:A3:92:19:87:D2:A2:A5:77:94:82:29:B9:5A:91:01:AB:5F:75:16:9A:BA:CD:3D:D3:69:3D:6A:87:DC:E8:0E" + * + * @param[in] cert The certificate which will be used to create the fingerprint + * @param[in] fdig The digest algorithm to use, defaults to SHA-256 b/c that is what ssl_options_t uses + * @return The human-readable fingerprint string + * + * @throw boost::system_error if there is an OpenSSL error + */ + std::string get_hr_ssl_fingerprint(const X509 *cert, const EVP_MD *fdig = EVP_sha256()); + + /** + * @brief Create a human-readable fingerprint from the contents of an X509 certificate + * + * Should be equivalent to the command `openssl x509 -in -fingerprint -sha256 -noout` + * Example output: "12:A3:92:19:87:D2:A2:A5:77:94:82:29:B9:5A:91:01:AB:5F:75:16:9A:BA:CD:3D:D3:69:3D:6A:87:DC:E8:0E" + * + * @param[in] cert_path The path to an X509 certificate which will be used to create the fingerprint + * @param[in] fdig The digest algorithm to use, defaults to SHA-256 b/c that is what ssl_options_t uses + * @return The human-readable fingerprint string + * + * @throw boost::system_error if there is an OpenSSL error or file I/O error + */ + std::string get_hr_ssl_fingerprint_from_file(const std::string& cert_path, const EVP_MD *fdig = EVP_sha256()); + //! Store private key for `ssl` at `base + ".key"` unencrypted and certificate for `ssl` at `base + ".crt"`. boost::system::error_code store_ssl_keys(boost::asio::ssl::context& ssl, const boost::filesystem::path& base); } diff --git a/contrib/epee/src/net_ssl.cpp b/contrib/epee/src/net_ssl.cpp index ff9c48c34..3822eb16d 100644 --- a/contrib/epee/src/net_ssl.cpp +++ b/contrib/epee/src/net_ssl.cpp @@ -641,6 +641,56 @@ bool ssl_options_t::handshake( return true; } +std::string get_hr_ssl_fingerprint(const X509 *cert, const EVP_MD *fdig) +{ + unsigned int j; + unsigned int n; + unsigned char md[EVP_MAX_MD_SIZE]; + std::string fingerprint; + + CHECK_AND_ASSERT_THROW_MES(cert && fdig, "Pointer args to get_hr_ssl_fingerprint cannot be null"); + + if (!X509_digest(cert, fdig, md, &n)) + { + const unsigned long ssl_err_val = static_cast(ERR_get_error()); + const boost::system::error_code ssl_err_code = boost::asio::error::ssl_errors(static_cast(ssl_err_val)); + MERROR("Failed to create SSL fingerprint: " << ERR_reason_error_string(ssl_err_val)); + throw boost::system::system_error(ssl_err_code, ERR_reason_error_string(ssl_err_val)); + } + fingerprint.resize(n * 3 - 1); + char *out = &fingerprint[0]; + for (j = 0; j < n; ++j) + { + snprintf(out, 3 + (j + 1 < n), "%02X%s", md[j], (j + 1 == n) ? "" : ":"); + out += 3; + } + return fingerprint; +} + +std::string get_hr_ssl_fingerprint_from_file(const std::string& cert_path, const EVP_MD *fdig) { + // Open file for reading + FILE* fp = fopen(cert_path.c_str(), "r"); + if (!fp) + { + const boost::system::error_code err_code(errno, boost::system::system_category()); + throw boost::system::system_error(err_code, "Failed to open certificate file '" + cert_path + "'"); + } + std::unique_ptr file(fp, &fclose); + + // Extract certificate structure from file + X509* ssl_cert_handle = PEM_read_X509(file.get(), NULL, NULL, NULL); + if (!ssl_cert_handle) { + const unsigned long ssl_err_val = static_cast(ERR_get_error()); + const boost::system::error_code ssl_err_code = boost::asio::error::ssl_errors(static_cast(ssl_err_val)); + MERROR("OpenSSL error occurred while loading certificate at '" + cert_path + "'"); + throw boost::system::system_error(ssl_err_code, ERR_reason_error_string(ssl_err_val)); + } + std::unique_ptr ssl_cert(ssl_cert_handle, &X509_free); + + // Get the fingerprint from X509 structure + return get_hr_ssl_fingerprint(ssl_cert.get(), fdig); +} + bool ssl_support_from_string(ssl_support_t &ssl, boost::string_ref s) { if (s == "enabled") @@ -705,6 +755,29 @@ boost::system::error_code store_ssl_keys(boost::asio::ssl::context& ssl, const b return boost::asio::error::ssl_errors(ERR_get_error()); if (std::fclose(file.release()) != 0) return {errno, boost::system::system_category()}; + + // write SHA-256 fingerprint file + const boost::filesystem::path fp_file{base.string() + ".fingerprint"}; + file.reset(std::fopen(fp_file.string().c_str(), "w")); + if (!file) + return {errno, boost::system::system_category()}; + const auto fp_perms = (boost::filesystem::owner_read | boost::filesystem::group_read | boost::filesystem::others_read); + boost::filesystem::permissions(fp_file, fp_perms, error); + if (error) + return error; + try + { + const std::string fingerprint = get_hr_ssl_fingerprint(ssl_cert); + if (fingerprint.length() != fwrite(fingerprint.c_str(), sizeof(char), fingerprint.length(), file.get())) + return {errno, boost::system::system_category()}; + } + catch (const boost::system::system_error& fperr) + { + return fperr.code(); + } + if (std::fclose(file.release()) != 0) + return {errno, boost::system::system_category()}; + return error; } diff --git a/src/gen_ssl_cert/gen_ssl_cert.cpp b/src/gen_ssl_cert/gen_ssl_cert.cpp index e695df727..b25d9a73d 100644 --- a/src/gen_ssl_cert/gen_ssl_cert.cpp +++ b/src/gen_ssl_cert/gen_ssl_cert.cpp @@ -65,29 +65,6 @@ namespace const command_line::arg_descriptor arg_prompt_for_passphrase = {"prompt-for-passphrase", gencert::tr("Prompt for a passphrase with which to encrypt the private key"), false}; } -// adapted from openssl's apps/x509.c -static std::string get_fingerprint(X509 *cert, const EVP_MD *fdig) -{ - unsigned int j; - unsigned int n; - unsigned char md[EVP_MAX_MD_SIZE]; - std::string fingerprint; - - if (!X509_digest(cert, fdig, md, &n)) - { - tools::fail_msg_writer() << tr("Failed to create fingerprint: ") << ERR_reason_error_string(ERR_get_error()); - return fingerprint; - } - fingerprint.resize(n * 3 - 1); - char *out = &fingerprint[0]; - for (j = 0; j < n; ++j) - { - snprintf(out, 3 + (j + 1 < n), "%02X%s", md[j], (j + 1 == n) ? "" : ":"); - out += 3; - } - return fingerprint; -} - int main(int argc, char* argv[]) { TRY_ENTRY(); @@ -246,7 +223,7 @@ int main(int argc, char* argv[]) tools::success_msg_writer() << tr("New certificate created:"); tools::success_msg_writer() << tr("Certificate: ") << certificate_filename; - tools::success_msg_writer() << tr("SHA-256 Fingerprint: ") << get_fingerprint(cert, EVP_sha256()); + tools::success_msg_writer() << tr("SHA-256 Fingerprint: ") << epee::net_utils::get_hr_ssl_fingerprint(cert); tools::success_msg_writer() << tr("Private key: ") << private_key_filename << " (" << (private_key_passphrase.empty() ? "unencrypted" : "encrypted") << ")"; return 0; diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index cb347110d..0adf0b65e 100644 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -352,6 +352,7 @@ namespace cryptonote const auto ssl_base_path = (boost::filesystem::path{data_dir} / "rpc_ssl").string(); const bool ssl_cert_file_exists = boost::filesystem::exists(ssl_base_path + ".crt"); const bool ssl_pkey_file_exists = boost::filesystem::exists(ssl_base_path + ".key"); + const bool ssl_fp_file_exists = boost::filesystem::exists(ssl_base_path + ".fingerprint"); if (store_ssl_key) { // .key files are often given different read permissions as their corresponding .crt files. @@ -361,13 +362,39 @@ namespace cryptonote MFATAL("Certificate (.crt) and private key (.key) files must both exist or both not exist at path: " << ssl_base_path); return false; } + else if (!ssl_cert_file_exists && ssl_fp_file_exists) // only fingerprint file is present + { + MFATAL("Fingerprint file is present while certificate (.crt) and private key (.key) files are not at path: " << ssl_base_path); + return false; + } else if (ssl_cert_file_exists) { // and ssl_pkey_file_exists // load key from previous run, password prompted by OpenSSL store_ssl_key = false; rpc_config->ssl_options.auth = epee::net_utils::ssl_authentication_t{ssl_base_path + ".key", ssl_base_path + ".crt"}; + + // Since the .fingerprint file was added afterwards, sometimes the other 2 are present, and .fingerprint isn't + // In that case, generate the .fingerprint file from the existing .crt file + if (!ssl_fp_file_exists) + { + try + { + std::string fingerprint = epee::net_utils::get_hr_ssl_fingerprint_from_file(ssl_base_path + ".crt"); + if (!epee::file_io_utils::save_string_to_file(ssl_base_path + ".fingerprint", fingerprint)) + { + MWARNING("Could not save SSL fingerprint to file '" << ssl_base_path << ".fingerprint'"); + } + const auto fp_perms = boost::filesystem::owner_read | boost::filesystem::group_read | boost::filesystem::others_read; + boost::filesystem::permissions(ssl_base_path + ".fingerprint", fp_perms); + } + catch (const std::exception& e) + { + // Do nothing. The fingerprint file is helpful, but not at all necessary. + MWARNING("While trying to store SSL fingerprint file, got error (ignoring): " << e.what()); + } + } } - } + } // if (store_ssl_key) auto rng = [](size_t len, uint8_t *ptr){ return crypto::rand(len, ptr); }; const bool inited = epee::http_server_impl_base::init(