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();