Add hostname listener for generating self-signed HTTPS certificate
- Add a hostname listener that will create a self-signed HTTPS
certificate with the appropriate subject when the BMC gets its
hostname assigned via IPMI. The "insecure-disable-ssl" must be
disabled for this feature to take effect.
Note:
- New self-signed certificate subject: C=US, O=OpenBMC, CN=${hostname}
- If the same hostname is assigned, it will not be triggered
- Only the self-signed certificate with Netscape Comment of
"Generated from OpenBMC service" will be replaced
Details about certificate key usage:
- NID_basic_constraints
The CA boolean indicates whether the certified public key may be
used to verify certificate signatures.
Refer to: https://tools.ietf.org/html/rfc5280#section-4.2.1.9
- NID_subject_alt_name
Although the use of the Common Name is existing practice, it is
deprecated and Certification Authorities are encouraged to use the
dNSName instead.
Refer to: https://tools.ietf.org/html/rfc2818#section-3.1
- NID_subject_key_identifier
The subject key identifier extension provides a means of
identifying certificates that contain a particular public key.
Refer to: https://tools.ietf.org/html/rfc5280#section-4.2.1.2
- NID_authority_key_identifier
The authority key identifier extension provides a means of
identifying the public key corresponding to the private key used
to sign a certificate.
Refer to: https://tools.ietf.org/html/rfc5280#section-4.2.1.1
- NID_key_usage
- NID_ext_key_usage
id-kp-serverAuth
-- TLS WWW server authentication
-- Key usage bits that may be consistent: digitalSignature,
-- keyEncipherment or keyAgreement
Refer to: https://tools.ietf.org/html/rfc5280#section-4.2.1.3
Refer to: https://tools.ietf.org/html/rfc5280#section-4.2.1.12
Tested:
- To test and verify the service is functionally working correctly,
we can use `openssl` and `ipmitool` to execute the following
commands:
- Assign BMC hostname
ipmitool -H $IP -I lanplus -U root -P 0penBmc -C 17 dcmi
set_mc_id_string $hostname
- Get BMC server certificate infomation
echo quit | openssl s_client -showcerts -servername $IP -connect
$IP:443
Signed-off-by: Alan Kuo <Alan_Kuo@quantatw.com>
Change-Id: I24aeb4d2fb46ff5f0cc1c6aa65984f46b0e1d3e2
diff --git a/include/hostname_monitor.hpp b/include/hostname_monitor.hpp
new file mode 100644
index 0000000..7b8283e
--- /dev/null
+++ b/include/hostname_monitor.hpp
@@ -0,0 +1,141 @@
+#pragma once
+#ifdef BMCWEB_ENABLE_SSL
+#include <boost/container/flat_map.hpp>
+#include <dbus_singleton.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <sdbusplus/message/types.hpp>
+#include <ssl_key_handler.hpp>
+
+namespace crow
+{
+namespace hostname_monitor
+{
+static std::unique_ptr<sdbusplus::bus::match::match> hostnameSignalMonitor;
+
+inline void installCertificate(const std::filesystem::path& certPath)
+{
+ crow::connections::systemBus->async_method_call(
+ [certPath](boost::system::error_code ec) {
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR << "Replace Certificate Fail..";
+ return;
+ }
+
+ BMCWEB_LOG_INFO << "Replace HTTPs Certificate Success, "
+ "remove temporary certificate file..";
+ remove(certPath.c_str());
+ },
+ "xyz.openbmc_project.Certs.Manager.Server.Https",
+ "/xyz/openbmc_project/certs/server/https/1",
+ "xyz.openbmc_project.Certs.Replace", "Replace", certPath.string());
+}
+
+inline int onPropertyUpdate(sd_bus_message* m, void* /* userdata */,
+ sd_bus_error* ret_error)
+{
+ if (ret_error == nullptr || sd_bus_error_is_set(ret_error))
+ {
+ BMCWEB_LOG_ERROR << "Got sdbus error on match";
+ return 0;
+ }
+
+ sdbusplus::message::message message(m);
+ std::string iface;
+ boost::container::flat_map<std::string, std::variant<std::string>>
+ changedProperties;
+
+ message.read(iface, changedProperties);
+ auto it = changedProperties.find("HostName");
+ if (it == changedProperties.end())
+ {
+ return 0;
+ }
+
+ std::string* hostname = std::get_if<std::string>(&it->second);
+ if (hostname == nullptr)
+ {
+ BMCWEB_LOG_ERROR << "Unable to read hostname";
+ return 0;
+ }
+
+ BMCWEB_LOG_DEBUG << "Read hostname from signal: " << *hostname;
+ const std::string certFile = "/etc/ssl/certs/https/server.pem";
+
+ X509* cert = ensuressl::loadCert(certFile);
+ if (cert == nullptr)
+ {
+ BMCWEB_LOG_ERROR << "Failed to read cert";
+ return 0;
+ }
+
+ const int maxKeySize = 256;
+ std::array<char, maxKeySize> cnBuffer{};
+
+ int cnLength =
+ X509_NAME_get_text_by_NID(X509_get_subject_name(cert), NID_commonName,
+ cnBuffer.data(), cnBuffer.size());
+ if (cnLength == -1)
+ {
+ BMCWEB_LOG_ERROR << "Failed to read NID_commonName";
+ X509_free(cert);
+ return 0;
+ }
+ std::string_view cnValue(std::begin(cnBuffer),
+ static_cast<size_t>(cnLength));
+
+ EVP_PKEY* pPubKey = X509_get_pubkey(cert);
+ if (pPubKey == nullptr)
+ {
+ BMCWEB_LOG_ERROR << "Failed to get public key";
+ X509_free(cert);
+ return 0;
+ }
+ int isSelfSigned = X509_verify(cert, pPubKey);
+ EVP_PKEY_free(pPubKey);
+
+ BMCWEB_LOG_DEBUG << "Current HTTPs Certificate Subject CN: " << cnValue
+ << ", New HostName: " << *hostname
+ << ", isSelfSigned: " << isSelfSigned;
+
+ ASN1_IA5STRING* asn1 = static_cast<ASN1_IA5STRING*>(
+ X509_get_ext_d2i(cert, NID_netscape_comment, nullptr, nullptr));
+ if (asn1)
+ {
+ std::string_view comment(reinterpret_cast<const char*>(asn1->data),
+ static_cast<size_t>(asn1->length));
+ BMCWEB_LOG_DEBUG << "x509Comment: " << comment;
+
+ if (ensuressl::x509Comment == comment && isSelfSigned == 1 &&
+ cnValue != *hostname)
+ {
+ BMCWEB_LOG_INFO << "Ready to generate new HTTPs "
+ << "certificate with subject cn: " << *hostname;
+
+ ensuressl::generateSslCertificate("/tmp/hostname_cert.tmp",
+ *hostname);
+ installCertificate("/tmp/hostname_cert.tmp");
+ }
+ ASN1_STRING_free(asn1);
+ }
+ X509_free(cert);
+ return 0;
+}
+
+inline void registerHostnameSignal()
+{
+ BMCWEB_LOG_INFO << "Register HostName PropertiesChanged Signal";
+ std::string propertiesMatchString =
+ ("type='signal',"
+ "interface='org.freedesktop.DBus.Properties',"
+ "path='/xyz/openbmc_project/network/config',"
+ "arg0='xyz.openbmc_project.Network.SystemConfiguration',"
+ "member='PropertiesChanged'");
+
+ hostnameSignalMonitor = std::make_unique<sdbusplus::bus::match::match>(
+ *crow::connections::systemBus, propertiesMatchString, onPropertyUpdate,
+ nullptr);
+}
+} // namespace hostname_monitor
+} // namespace crow
+#endif
diff --git a/include/ssl_key_handler.hpp b/include/ssl_key_handler.hpp
index 19d3ec7..93fe5d0 100644
--- a/include/ssl_key_handler.hpp
+++ b/include/ssl_key_handler.hpp
@@ -18,6 +18,7 @@
namespace ensuressl
{
constexpr char const* trustStorePath = "/etc/ssl/certs/authority";
+constexpr char const* x509Comment = "Generated from OpenBMC service";
static void initOpenssl();
static EVP_PKEY* createEcKey();
@@ -170,7 +171,56 @@
return certValid;
}
-inline void generateSslCertificate(const std::string& filepath)
+inline X509* loadCert(const std::string& filePath)
+{
+ BIO* certFileBio = BIO_new_file(filePath.c_str(), "rb");
+ if (!certFileBio)
+ {
+ BMCWEB_LOG_ERROR << "Error occured during BIO_new_file call, "
+ << "FILE= " << filePath;
+ return nullptr;
+ }
+
+ X509* cert = X509_new();
+ if (!cert)
+ {
+ BMCWEB_LOG_ERROR << "Error occured during X509_new call, "
+ << ERR_get_error();
+ BIO_free(certFileBio);
+ return nullptr;
+ }
+
+ if (!PEM_read_bio_X509(certFileBio, &cert, nullptr, nullptr))
+ {
+ BMCWEB_LOG_ERROR << "Error occured during PEM_read_bio_X509 call, "
+ << "FILE= " << filePath;
+
+ BIO_free(certFileBio);
+ X509_free(cert);
+ return nullptr;
+ }
+ return cert;
+}
+
+inline int addExt(X509* cert, int nid, const char* value)
+{
+ X509_EXTENSION* ex = nullptr;
+ X509V3_CTX ctx;
+ X509V3_set_ctx_nodb(&ctx);
+ X509V3_set_ctx(&ctx, cert, cert, nullptr, nullptr, 0);
+ ex = X509V3_EXT_conf_nid(nullptr, &ctx, nid, const_cast<char*>(value));
+ if (!ex)
+ {
+ BMCWEB_LOG_ERROR << "Error: In X509V3_EXT_conf_nidn: " << value;
+ return -1;
+ }
+ X509_add_ext(cert, ex, -1);
+ X509_EXTENSION_free(ex);
+ return 0;
+}
+
+inline void generateSslCertificate(const std::string& filepath,
+ const std::string& cn)
{
FILE* pFile = nullptr;
std::cout << "Generating new keys\n";
@@ -189,8 +239,10 @@
// get a random number from the RNG for the certificate serial
// number If this is not random, regenerating certs throws broswer
// errors
- std::random_device rd;
- int serial = static_cast<int>(rd());
+ bmcweb::OpenSSLGenerator gen;
+ std::uniform_int_distribution<int> dis(
+ 1, std::numeric_limits<int>::max());
+ int serial = dis(gen);
ASN1_INTEGER_set(X509_get_serialNumber(x509), serial);
@@ -215,10 +267,19 @@
reinterpret_cast<const unsigned char*>("OpenBMC"), -1, -1, 0);
X509_NAME_add_entry_by_txt(
name, "CN", MBSTRING_ASC,
- reinterpret_cast<const unsigned char*>("testhost"), -1, -1, 0);
+ reinterpret_cast<const unsigned char*>(cn.c_str()), -1, -1, 0);
// set the CSR options
X509_set_issuer_name(x509, name);
+ X509_set_version(x509, 2);
+ addExt(x509, NID_basic_constraints, ("critical,CA:TRUE"));
+ addExt(x509, NID_subject_alt_name, ("DNS:" + cn).c_str());
+ addExt(x509, NID_subject_key_identifier, ("hash"));
+ addExt(x509, NID_authority_key_identifier, ("keyid"));
+ addExt(x509, NID_key_usage, ("digitalSignature, keyEncipherment"));
+ addExt(x509, NID_ext_key_usage, ("serverAuth"));
+ addExt(x509, NID_netscape_comment, (x509Comment));
+
// Sign the certificate with our private key
X509_sign(x509, pPrivKey, EVP_sha256());
@@ -289,7 +350,7 @@
if (!pemFileValid)
{
std::cerr << "Error in verifying signature, regenerating\n";
- generateSslCertificate(filepath);
+ generateSslCertificate(filepath, "testhost");
}
}
diff --git a/src/webserver_main.cpp b/src/webserver_main.cpp
index 3e796c2..b5bc28c 100644
--- a/src/webserver_main.cpp
+++ b/src/webserver_main.cpp
@@ -4,6 +4,7 @@
#include <boost/asio/io_context.hpp>
#include <dbus_monitor.hpp>
#include <dbus_singleton.hpp>
+#include <hostname_monitor.hpp>
#include <ibm/management_console_rest.hpp>
#include <image_upload.hpp>
#include <kvm_websocket.hpp>
@@ -124,6 +125,11 @@
}
#endif
+#ifdef BMCWEB_ENABLE_SSL
+ BMCWEB_LOG_INFO << "Start Hostname Monitor Service...";
+ crow::hostname_monitor::registerHostnameSignal();
+#endif
+
app.run();
io->run();