Redfish: GenerateCSR action for HTTPS certificate

Implemented GenerateCSR action of CertificateService
for generating CSR of HTTPS certificate

CSR requests initiated through D-Bus are time-consuming
and might result D-Bus time-out error

GenerateCSR request is performed in child process in
the backend so that caller is returned immediately.

Caller need to register for "InterfacesAdded" signal
generated when a new CSR object is creatd by backend
after completion of the CSR request.

Caller initiates read on the CSR object created to
read the CSR string.

Timer is added to cancel the operation if "Interfaces
Added" signal is not received in a specified time.

Modified to support only 2048 keybit length due to
time taken in private key generation.

Tested
1) Tested schema with validator and no issues
2)
curl -c cjar -b cjar -k -H "X-Auth-Token: $bmc_token" -X POST
https://${bmc}/redfish/v1/CertificateService/Actions/CertificateService.GenerateCSR/
-d @generate_https.json
{
  "CSRString": "-----BEGIN CERTIFICATE ..."
  "CertificateCollection": {
    "@odata.id": "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/"
  }
}
3. generate_https.json
{
    "City": "Austin",
    "CertificateCollection": {
        "@odata.id":
"/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/"
    },
    "CommonName": "www.ibm.com",
    "ContactPerson":"myname",
    "AlternativeNames":["www.ibm.com"],
    "ChallengePassword":"",
    "Email":"openbmc@in.ibm.com",
    "GivenName":"",
    "Initials":"",
    "Country": "US",
    "KeyCurveId":"",
    "KeyUsage":["KeyAgreement"],
    "KeyBitLength": 1024,
    "KeyPairAlgorithm": "RSA",
    "Organization": "IBM",
    "OrganizationalUnit": "ISL",
    "State": "AU",
    "Surname": "",
    "UnstructuredName": ""
}
4) Verified Required and Optional parameters
5) Generate EC CSR with curve ID secp224r1
curl -c cjar -b cjar -k -H "X-Auth-Token: $bmc_token" -X POST
https://${bmc}/redfish/v1/CertificateService/Actions/CertificateService.GenerateCSR/
-d @generate_https.json
{
  "CSRString": "-----BEGIN CERTIFICATE
REQUEST-----\nMIIBQzCB8wIBATCBmzEUMBIGA1UdEQwLd3d3LmlibS5jb20xDzANBgNVBAcMBkF1\nc3RpbjEUMBIGA1UEAwwLd3d3LmlibS5jb20xDzANBgNVBCkMBm15bmFtZTELMAkG\nA1UEBhMCVVMxDDAKBgQrDgMCDAJFQzEVMBMGA1UdDwwMS2V5QWdyZWVtZW50MQww\nCgYDVQQKDANJQk0xCzAJBgNVBAgMAkFVME4wEAYHKoZIzj0CAQYFK4EEACEDOgAE\n7hyL8FWmeCBRpCxWKjlce9nRghwS0lBrBdslOZ+n9+hFD+0KD8L+BORwm7dfzGlG\nTblh2G6cQ8KgADAKBggqhkjOPQQDAgM/ADA8Ahw1nlGdEFfnb+2zxdfVeTQYgCTw\nNos0t2rsGc/zAhxS9/paXZtVqR+WzdQVsjSLC/BedbXv1EmW52Uo\n-----END
CERTIFICATE REQUEST-----\n",
  "CertificateCollection": {
    "@odata.id": "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/"
  }
}
Change-Id: I2528c802ff3c6f63570cdb355b9c1195797a0e53
Signed-off-by: Marri Devender Rao <devenrao@in.ibm.com>
diff --git a/redfish-core/include/redfish.hpp b/redfish-core/include/redfish.hpp
index 6f68611..93418e4 100644
--- a/redfish-core/include/redfish.hpp
+++ b/redfish-core/include/redfish.hpp
@@ -134,10 +134,9 @@
         nodes.emplace_back(std::make_unique<HTTPSCertificate>(app));
         nodes.emplace_back(std::make_unique<LDAPCertificateCollection>(app));
         nodes.emplace_back(std::make_unique<LDAPCertificate>(app));
-
+        nodes.emplace_back(std::make_unique<CertificateActionGenerateCSR>(app));
         nodes.emplace_back(std::make_unique<SystemPCIeFunction>(app));
         nodes.emplace_back(std::make_unique<SystemPCIeDevice>(app));
-
         for (const auto& node : nodes)
         {
             node->initPrivileges();
diff --git a/redfish-core/lib/certificate_service.hpp b/redfish-core/lib/certificate_service.hpp
index d3eaec1..712baf1 100644
--- a/redfish-core/lib/certificate_service.hpp
+++ b/redfish-core/lib/certificate_service.hpp
@@ -29,9 +29,6 @@
 constexpr char const *certPropIntf = "xyz.openbmc_project.Certs.Certificate";
 constexpr char const *dbusPropIntf = "org.freedesktop.DBus.Properties";
 constexpr char const *dbusObjManagerIntf = "org.freedesktop.DBus.ObjectManager";
-constexpr char const *mapperBusName = "xyz.openbmc_project.ObjectMapper";
-constexpr char const *mapperObjectPath = "/xyz/openbmc_project/object_mapper";
-constexpr char const *mapperIntf = "xyz.openbmc_project.ObjectMapper";
 constexpr char const *ldapObjectPath = "/xyz/openbmc_project/certs/client/ldap";
 constexpr char const *httpsServiceName =
     "xyz.openbmc_project.Certs.Manager.Server.Https";
@@ -82,6 +79,9 @@
             {"target", "/redfish/v1/CertificateService/Actions/"
                        "CertificateService.ReplaceCertificate"},
             {"CertificateType@Redfish.AllowableValues", {"PEM"}}};
+        res.jsonValue["Actions"]["#CertificateService.GenerateCSR"] = {
+            {"target", "/redfish/v1/CertificateService/Actions/"
+                       "CertificateService.GenerateCSR"}};
         res.end();
     }
 }; // CertificateService
@@ -166,6 +166,281 @@
     std::filesystem::path certDirectory;
 };
 
+static std::unique_ptr<sdbusplus::bus::match::match> csrMatcher;
+/**
+ * @brief Read data from CSR D-bus object and set to response
+ *
+ * @param[in] asyncResp Shared pointer to the response message
+ * @param[in] certURI Link to certifiate collection URI
+ * @param[in] service D-Bus service name
+ * @param[in] certObjPath certificate D-Bus object path
+ * @param[in] csrObjPath CSR D-Bus object path
+ * @return None
+ */
+static void getCSR(const std::shared_ptr<AsyncResp> &asyncResp,
+                   const std::string &certURI, const std::string &service,
+                   const std::string &certObjPath,
+                   const std::string &csrObjPath)
+{
+    BMCWEB_LOG_DEBUG << "getCSR CertObjectPath" << certObjPath
+                     << " CSRObjectPath=" << csrObjPath
+                     << " service=" << service;
+    crow::connections::systemBus->async_method_call(
+        [asyncResp, certURI](const boost::system::error_code ec,
+                             const std::string &csr) {
+            if (ec)
+            {
+                BMCWEB_LOG_ERROR << "DBUS response error: " << ec;
+                messages::internalError(asyncResp->res);
+                return;
+            }
+            if (csr.empty())
+            {
+                BMCWEB_LOG_ERROR << "CSR read is empty";
+                messages::internalError(asyncResp->res);
+                return;
+            }
+            asyncResp->res.jsonValue["CSRString"] = csr;
+            asyncResp->res.jsonValue["CertificateCollection"] = {
+                {"@odata.id", certURI}};
+        },
+        service, csrObjPath, "xyz.openbmc_project.Certs.CSR", "CSR");
+}
+
+/**
+ * Action to Generate CSR
+ */
+class CertificateActionGenerateCSR : public Node
+{
+  public:
+    CertificateActionGenerateCSR(CrowApp &app) :
+        Node(app, "/redfish/v1/CertificateService/Actions/"
+                  "CertificateService.GenerateCSR/")
+    {
+        entityPrivileges = {
+            {boost::beast::http::verb::get, {{"Login"}}},
+            {boost::beast::http::verb::head, {{"Login"}}},
+            {boost::beast::http::verb::patch, {{"ConfigureComponents"}}},
+            {boost::beast::http::verb::put, {{"ConfigureComponents"}}},
+            {boost::beast::http::verb::delete_, {{"ConfigureComponents"}}},
+            {boost::beast::http::verb::post, {{"ConfigureComponents"}}}};
+    }
+
+  private:
+    void doPost(crow::Response &res, const crow::Request &req,
+                const std::vector<std::string> &params) override
+    {
+        static const int RSA_KEY_BIT_LENGTH = 2048;
+        auto asyncResp = std::make_shared<AsyncResp>(res);
+        // Required parameters
+        std::string city;
+        std::string commonName;
+        std::string country;
+        std::string organization;
+        std::string organizationalUnit;
+        std::string state;
+        nlohmann::json certificateCollection;
+
+        // Optional parameters
+        std::optional<std::vector<std::string>> optAlternativeNames =
+            std::vector<std::string>();
+        std::optional<std::string> optContactPerson = "";
+        std::optional<std::string> optChallengePassword = "";
+        std::optional<std::string> optEmail = "";
+        std::optional<std::string> optGivenName = "";
+        std::optional<std::string> optInitials = "";
+        std::optional<int64_t> optKeyBitLength = RSA_KEY_BIT_LENGTH;
+        std::optional<std::string> optKeyCurveId = "prime256v1";
+        std::optional<std::string> optKeyPairAlgorithm = "EC";
+        std::optional<std::vector<std::string>> optKeyUsage =
+            std::vector<std::string>();
+        std::optional<std::string> optSurname = "";
+        std::optional<std::string> optUnstructuredName = "";
+        if (!json_util::readJson(
+                req, asyncResp->res, "City", city, "CommonName", commonName,
+                "ContactPerson", optContactPerson, "Country", country,
+                "Organization", organization, "OrganizationalUnit",
+                organizationalUnit, "State", state, "CertificateCollection",
+                certificateCollection, "AlternativeNames", optAlternativeNames,
+                "ChallengePassword", optChallengePassword, "Email", optEmail,
+                "GivenName", optGivenName, "Initials", optInitials,
+                "KeyBitLength", optKeyBitLength, "KeyCurveId", optKeyCurveId,
+                "KeyPairAlgorithm", optKeyPairAlgorithm, "KeyUsage",
+                optKeyUsage, "Surname", optSurname, "UnstructuredName",
+                optUnstructuredName))
+        {
+            return;
+        }
+
+        // bmcweb has no way to store or decode a private key challenge
+        // password, which will likely cause bmcweb to crash on startup if this
+        // is not set on a post so not allowing the user to set value
+        if (*optChallengePassword != "")
+        {
+            messages::actionParameterNotSupported(asyncResp->res, "GenerateCSR",
+                                                  "ChallengePassword");
+            return;
+        }
+
+        std::string certURI;
+        if (!redfish::json_util::readJson(certificateCollection, asyncResp->res,
+                                          "@odata.id", certURI))
+        {
+            return;
+        }
+
+        std::string objectPath;
+        std::string service;
+        if (boost::starts_with(
+                certURI,
+                "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates"))
+        {
+            objectPath = certs::httpsObjectPath;
+            service = certs::httpsServiceName;
+        }
+        else
+        {
+            messages::actionParameterNotSupported(
+                asyncResp->res, "CertificateCollection", "GenerateCSR");
+            return;
+        }
+
+        // supporting only EC and RSA algorithm
+        if (*optKeyPairAlgorithm != "EC" && *optKeyPairAlgorithm != "RSA")
+        {
+            messages::actionParameterNotSupported(
+                asyncResp->res, "KeyPairAlgorithm", "GenerateCSR");
+            return;
+        }
+
+        // supporting only 2048 key bit length for RSA algorithm due to time
+        // consumed in generating private key
+        if (*optKeyPairAlgorithm == "RSA" &&
+            *optKeyBitLength != RSA_KEY_BIT_LENGTH)
+        {
+            messages::propertyValueNotInList(asyncResp->res,
+                                             std::to_string(*optKeyBitLength),
+                                             "KeyBitLength");
+            return;
+        }
+
+        // validate KeyUsage supporting only 1 type based on URL
+        if (boost::starts_with(
+                certURI,
+                "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates"))
+        {
+            if (optKeyUsage->size() == 0)
+            {
+                optKeyUsage->push_back("ServerAuthentication");
+            }
+            else if (optKeyUsage->size() == 1)
+            {
+                if ((*optKeyUsage)[0] != "ServerAuthentication")
+                {
+                    messages::propertyValueNotInList(
+                        asyncResp->res, (*optKeyUsage)[0], "KeyUsage");
+                    return;
+                }
+            }
+            else
+            {
+                messages::actionParameterNotSupported(
+                    asyncResp->res, "KeyUsage", "GenerateCSR");
+                return;
+            }
+        }
+
+        // Only allow one CSR matcher at a time so setting retry time-out and
+        // timer expiry to 10 seconds for now.
+        static const int TIME_OUT = 10;
+        if (csrMatcher)
+        {
+            res.addHeader("Retry-After", std::to_string(TIME_OUT));
+            messages::serviceTemporarilyUnavailable(asyncResp->res,
+                                                    std::to_string(TIME_OUT));
+            return;
+        }
+
+        // Make this static so it survives outside this method
+        static boost::asio::steady_timer timeout(*req.ioService);
+        timeout.expires_after(std::chrono::seconds(TIME_OUT));
+        timeout.async_wait([asyncResp](const boost::system::error_code &ec) {
+            csrMatcher = nullptr;
+            if (ec)
+            {
+                // operation_aborted is expected if timer is canceled before
+                // completion.
+                if (ec != boost::asio::error::operation_aborted)
+                {
+                    BMCWEB_LOG_ERROR << "Async_wait failed " << ec;
+                }
+                return;
+            }
+            BMCWEB_LOG_ERROR << "Timed out waiting for Generating CSR";
+            messages::internalError(asyncResp->res);
+        });
+
+        // create a matcher to wait on CSR object
+        BMCWEB_LOG_DEBUG << "create matcher with path " << objectPath;
+        std::string match("type='signal',"
+                          "interface='org.freedesktop.DBus.ObjectManager',"
+                          "path='" +
+                          objectPath +
+                          "',"
+                          "member='InterfacesAdded'");
+        csrMatcher = std::make_unique<sdbusplus::bus::match::match>(
+            *crow::connections::systemBus, match,
+            [asyncResp, service, objectPath,
+             certURI](sdbusplus::message::message &m) {
+                boost::system::error_code ec;
+                timeout.cancel(ec);
+                if (ec)
+                {
+                    BMCWEB_LOG_ERROR << "error canceling timer " << ec;
+                    csrMatcher = nullptr;
+                }
+                if (m.is_method_error())
+                {
+                    BMCWEB_LOG_ERROR << "Dbus method error!!!";
+                    messages::internalError(asyncResp->res);
+                    return;
+                }
+                std::vector<std::pair<
+                    std::string, std::vector<std::pair<
+                                     std::string, std::variant<std::string>>>>>
+                    interfacesProperties;
+                sdbusplus::message::object_path csrObjectPath;
+                m.read(csrObjectPath, interfacesProperties);
+                BMCWEB_LOG_DEBUG << "CSR object added" << csrObjectPath.str;
+                for (auto &interface : interfacesProperties)
+                {
+                    if (interface.first == "xyz.openbmc_project.Certs.CSR")
+                    {
+                        getCSR(asyncResp, certURI, service, objectPath,
+                               csrObjectPath.str);
+                        break;
+                    }
+                }
+            });
+        crow::connections::systemBus->async_method_call(
+            [asyncResp](const boost::system::error_code ec,
+                        const std::string &path) {
+                if (ec)
+                {
+                    BMCWEB_LOG_ERROR << "DBUS response error: " << ec.message();
+                    messages::internalError(asyncResp->res);
+                    return;
+                }
+            },
+            service, objectPath, "xyz.openbmc_project.Certs.CSR.Create",
+            "GenerateCSR", *optAlternativeNames, *optChallengePassword, city,
+            commonName, *optContactPerson, country, *optEmail, *optGivenName,
+            *optInitials, *optKeyBitLength, *optKeyCurveId,
+            *optKeyPairAlgorithm, *optKeyUsage, organization,
+            organizationalUnit, state, *optSurname, *optUnstructuredName);
+    }
+}; // CertificateActionGenerateCSR
+
 /**
  * @brief Parse and update Certficate Issue/Subject property
  *