xref: /openbmc/bmcweb/features/redfish/lib/certificate_service.hpp (revision 11ba39793c016e8476e44be880fb267e059dd4eb)
1 #pragma once
2 
3 #include <app.hpp>
4 #include <boost/convert.hpp>
5 #include <boost/convert/strtol.hpp>
6 #include <boost/system/linux_error.hpp>
7 #include <dbus_utility.hpp>
8 #include <query.hpp>
9 #include <registries/privilege_registry.hpp>
10 
11 namespace redfish
12 {
13 namespace certs
14 {
15 constexpr char const* certInstallIntf = "xyz.openbmc_project.Certs.Install";
16 constexpr char const* certReplaceIntf = "xyz.openbmc_project.Certs.Replace";
17 constexpr char const* objDeleteIntf = "xyz.openbmc_project.Object.Delete";
18 constexpr char const* certPropIntf = "xyz.openbmc_project.Certs.Certificate";
19 constexpr char const* dbusPropIntf = "org.freedesktop.DBus.Properties";
20 constexpr char const* dbusObjManagerIntf = "org.freedesktop.DBus.ObjectManager";
21 constexpr char const* httpsServiceName =
22     "xyz.openbmc_project.Certs.Manager.Server.Https";
23 constexpr char const* ldapServiceName =
24     "xyz.openbmc_project.Certs.Manager.Client.Ldap";
25 constexpr char const* authorityServiceName =
26     "xyz.openbmc_project.Certs.Manager.Authority.Ldap";
27 constexpr char const* baseObjectPath = "/xyz/openbmc_project/certs";
28 constexpr char const* httpsObjectPath =
29     "/xyz/openbmc_project/certs/server/https";
30 constexpr char const* ldapObjectPath = "/xyz/openbmc_project/certs/client/ldap";
31 constexpr char const* authorityObjectPath =
32     "/xyz/openbmc_project/certs/authority/ldap";
33 } // namespace certs
34 
35 /**
36  * The Certificate schema defines a Certificate Service which represents the
37  * actions available to manage certificates and links to where certificates
38  * are installed.
39  */
40 
41 // TODO: Issue#61 No entries are available for Certificate
42 // service at https://www.dmtf.org/standards/redfish
43 // "redfish standard registries". Need to modify after DMTF
44 // publish Privilege details for certificate service
45 
46 inline void requestRoutesCertificateService(App& app)
47 {
48     BMCWEB_ROUTE(app, "/redfish/v1/CertificateService/")
49         .privileges(redfish::privileges::getCertificateService)
50         .methods(boost::beast::http::verb::get)(
51             [&app](const crow::Request& req,
52                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
53         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
54         {
55             return;
56         }
57 
58         asyncResp->res.jsonValue["@odata.type"] =
59             "#CertificateService.v1_0_0.CertificateService";
60         asyncResp->res.jsonValue["@odata.id"] =
61             "/redfish/v1/CertificateService";
62         asyncResp->res.jsonValue["Id"] = "CertificateService";
63         asyncResp->res.jsonValue["Name"] = "Certificate Service";
64         asyncResp->res.jsonValue["Description"] =
65             "Actions available to manage certificates";
66         // /redfish/v1/CertificateService/CertificateLocations is something
67         // only ConfigureManager can access then only display when the user
68         // has permissions ConfigureManager
69         Privileges effectiveUserPrivileges =
70             redfish::getUserPrivileges(req.userRole);
71         if (isOperationAllowedWithPrivileges({{"ConfigureManager"}},
72                                              effectiveUserPrivileges))
73         {
74             asyncResp->res.jsonValue["CertificateLocations"]["@odata.id"] =
75                 "/redfish/v1/CertificateService/CertificateLocations";
76         }
77         asyncResp->res
78             .jsonValue["Actions"]["#CertificateService.ReplaceCertificate"] = {
79             {"target",
80              "/redfish/v1/CertificateService/Actions/CertificateService.ReplaceCertificate"},
81             {"CertificateType@Redfish.AllowableValues", {"PEM"}}};
82         asyncResp->res
83             .jsonValue["Actions"]["#CertificateService.GenerateCSR"] = {
84             {"target",
85              "/redfish/v1/CertificateService/Actions/CertificateService.GenerateCSR"}};
86         });
87 } // requestRoutesCertificateService
88 
89 /**
90  * @brief Find the ID specified in the URL
91  * Finds the numbers specified after the last "/" in the URL and returns.
92  * @param[in] path URL
93  * @return -1 on failure and number on success
94  */
95 inline long getIDFromURL(const std::string_view url)
96 {
97     std::size_t found = url.rfind('/');
98     if (found == std::string::npos)
99     {
100         return -1;
101     }
102 
103     if ((found + 1) < url.length())
104     {
105         std::string_view str = url.substr(found + 1);
106 
107         return boost::convert<long>(str, boost::cnv::strtol()).value_or(-1);
108     }
109 
110     return -1;
111 }
112 
113 inline std::string getCertificateFromReqBody(
114     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
115     const crow::Request& req)
116 {
117     nlohmann::json reqJson = nlohmann::json::parse(req.body, nullptr, false);
118 
119     if (reqJson.is_discarded())
120     {
121         // We did not receive JSON request, proceed as it is RAW data
122         return req.body;
123     }
124 
125     std::string certificate;
126     std::optional<std::string> certificateType = "PEM";
127 
128     if (!json_util::readJsonPatch(req, asyncResp->res, "CertificateString",
129                                   certificate, "CertificateType",
130                                   certificateType))
131     {
132         BMCWEB_LOG_ERROR << "Required parameters are missing";
133         messages::internalError(asyncResp->res);
134         return {};
135     }
136 
137     if (*certificateType != "PEM")
138     {
139         messages::propertyValueNotInList(asyncResp->res, *certificateType,
140                                          "CertificateType");
141         return {};
142     }
143 
144     return certificate;
145 }
146 
147 /**
148  * Class to create a temporary certificate file for uploading to system
149  */
150 class CertificateFile
151 {
152   public:
153     CertificateFile() = delete;
154     CertificateFile(const CertificateFile&) = delete;
155     CertificateFile& operator=(const CertificateFile&) = delete;
156     CertificateFile(CertificateFile&&) = delete;
157     CertificateFile& operator=(CertificateFile&&) = delete;
158     explicit CertificateFile(const std::string& certString)
159     {
160         std::array<char, 18> dirTemplate = {'/', 't', 'm', 'p', '/', 'C',
161                                             'e', 'r', 't', 's', '.', 'X',
162                                             'X', 'X', 'X', 'X', 'X', '\0'};
163         char* tempDirectory = mkdtemp(dirTemplate.data());
164         if (tempDirectory != nullptr)
165         {
166             certDirectory = tempDirectory;
167             certificateFile = certDirectory / "cert.pem";
168             std::ofstream out(certificateFile, std::ofstream::out |
169                                                    std::ofstream::binary |
170                                                    std::ofstream::trunc);
171             out << certString;
172             out.close();
173             BMCWEB_LOG_DEBUG << "Creating certificate file"
174                              << certificateFile.string();
175         }
176     }
177     ~CertificateFile()
178     {
179         if (std::filesystem::exists(certDirectory))
180         {
181             BMCWEB_LOG_DEBUG << "Removing certificate file"
182                              << certificateFile.string();
183             std::error_code ec;
184             std::filesystem::remove_all(certDirectory, ec);
185             if (ec)
186             {
187                 BMCWEB_LOG_ERROR << "Failed to remove temp directory"
188                                  << certDirectory.string();
189             }
190         }
191     }
192     std::string getCertFilePath()
193     {
194         return certificateFile;
195     }
196 
197   private:
198     std::filesystem::path certificateFile;
199     std::filesystem::path certDirectory;
200 };
201 
202 static std::unique_ptr<sdbusplus::bus::match::match> csrMatcher;
203 /**
204  * @brief Read data from CSR D-bus object and set to response
205  *
206  * @param[in] asyncResp Shared pointer to the response message
207  * @param[in] certURI Link to certifiate collection URI
208  * @param[in] service D-Bus service name
209  * @param[in] certObjPath certificate D-Bus object path
210  * @param[in] csrObjPath CSR D-Bus object path
211  * @return None
212  */
213 static void getCSR(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
214                    const std::string& certURI, const std::string& service,
215                    const std::string& certObjPath,
216                    const std::string& csrObjPath)
217 {
218     BMCWEB_LOG_DEBUG << "getCSR CertObjectPath" << certObjPath
219                      << " CSRObjectPath=" << csrObjPath
220                      << " service=" << service;
221     crow::connections::systemBus->async_method_call(
222         [asyncResp, certURI](const boost::system::error_code ec,
223                              const std::string& csr) {
224         if (ec)
225         {
226             BMCWEB_LOG_ERROR << "DBUS response error: " << ec;
227             messages::internalError(asyncResp->res);
228             return;
229         }
230         if (csr.empty())
231         {
232             BMCWEB_LOG_ERROR << "CSR read is empty";
233             messages::internalError(asyncResp->res);
234             return;
235         }
236         asyncResp->res.jsonValue["CSRString"] = csr;
237         asyncResp->res.jsonValue["CertificateCollection"]["@odata.id"] =
238             certURI;
239         },
240         service, csrObjPath, "xyz.openbmc_project.Certs.CSR", "CSR");
241 }
242 
243 /**
244  * Action to Generate CSR
245  */
246 inline void requestRoutesCertificateActionGenerateCSR(App& app)
247 {
248     BMCWEB_ROUTE(
249         app,
250         "/redfish/v1/CertificateService/Actions/CertificateService.GenerateCSR/")
251         .privileges(redfish::privileges::postCertificateService)
252         .methods(boost::beast::http::verb::post)(
253             [&app](const crow::Request& req,
254                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
255         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
256         {
257             return;
258         }
259         static const int rsaKeyBitLength = 2048;
260 
261         // Required parameters
262         std::string city;
263         std::string commonName;
264         std::string country;
265         std::string organization;
266         std::string organizationalUnit;
267         std::string state;
268         nlohmann::json certificateCollection;
269 
270         // Optional parameters
271         std::optional<std::vector<std::string>> optAlternativeNames =
272             std::vector<std::string>();
273         std::optional<std::string> optContactPerson = "";
274         std::optional<std::string> optChallengePassword = "";
275         std::optional<std::string> optEmail = "";
276         std::optional<std::string> optGivenName = "";
277         std::optional<std::string> optInitials = "";
278         std::optional<int64_t> optKeyBitLength = rsaKeyBitLength;
279         std::optional<std::string> optKeyCurveId = "secp384r1";
280         std::optional<std::string> optKeyPairAlgorithm = "EC";
281         std::optional<std::vector<std::string>> optKeyUsage =
282             std::vector<std::string>();
283         std::optional<std::string> optSurname = "";
284         std::optional<std::string> optUnstructuredName = "";
285         if (!json_util::readJsonAction(
286                 req, asyncResp->res, "City", city, "CommonName", commonName,
287                 "ContactPerson", optContactPerson, "Country", country,
288                 "Organization", organization, "OrganizationalUnit",
289                 organizationalUnit, "State", state, "CertificateCollection",
290                 certificateCollection, "AlternativeNames", optAlternativeNames,
291                 "ChallengePassword", optChallengePassword, "Email", optEmail,
292                 "GivenName", optGivenName, "Initials", optInitials,
293                 "KeyBitLength", optKeyBitLength, "KeyCurveId", optKeyCurveId,
294                 "KeyPairAlgorithm", optKeyPairAlgorithm, "KeyUsage",
295                 optKeyUsage, "Surname", optSurname, "UnstructuredName",
296                 optUnstructuredName))
297         {
298             return;
299         }
300 
301         // bmcweb has no way to store or decode a private key challenge
302         // password, which will likely cause bmcweb to crash on startup
303         // if this is not set on a post so not allowing the user to set
304         // value
305         if (!optChallengePassword->empty())
306         {
307             messages::actionParameterNotSupported(asyncResp->res, "GenerateCSR",
308                                                   "ChallengePassword");
309             return;
310         }
311 
312         std::string certURI;
313         if (!redfish::json_util::readJson(certificateCollection, asyncResp->res,
314                                           "@odata.id", certURI))
315         {
316             return;
317         }
318 
319         std::string objectPath;
320         std::string service;
321         if (certURI.starts_with(
322                 "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates"))
323         {
324             objectPath = certs::httpsObjectPath;
325             service = certs::httpsServiceName;
326         }
327         else if (certURI.starts_with(
328                      "/redfish/v1/AccountService/LDAP/Certificates"))
329         {
330             objectPath = certs::ldapObjectPath;
331             service = certs::ldapServiceName;
332         }
333         else
334         {
335             messages::actionParameterNotSupported(
336                 asyncResp->res, "CertificateCollection", "GenerateCSR");
337             return;
338         }
339 
340         // supporting only EC and RSA algorithm
341         if (*optKeyPairAlgorithm != "EC" && *optKeyPairAlgorithm != "RSA")
342         {
343             messages::actionParameterNotSupported(
344                 asyncResp->res, "KeyPairAlgorithm", "GenerateCSR");
345             return;
346         }
347 
348         // supporting only 2048 key bit length for RSA algorithm due to
349         // time consumed in generating private key
350         if (*optKeyPairAlgorithm == "RSA" &&
351             *optKeyBitLength != rsaKeyBitLength)
352         {
353             messages::propertyValueNotInList(asyncResp->res,
354                                              std::to_string(*optKeyBitLength),
355                                              "KeyBitLength");
356             return;
357         }
358 
359         // validate KeyUsage supporting only 1 type based on URL
360         if (certURI.starts_with(
361                 "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates"))
362         {
363             if (optKeyUsage->empty())
364             {
365                 optKeyUsage->push_back("ServerAuthentication");
366             }
367             else if (optKeyUsage->size() == 1)
368             {
369                 if ((*optKeyUsage)[0] != "ServerAuthentication")
370                 {
371                     messages::propertyValueNotInList(
372                         asyncResp->res, (*optKeyUsage)[0], "KeyUsage");
373                     return;
374                 }
375             }
376             else
377             {
378                 messages::actionParameterNotSupported(
379                     asyncResp->res, "KeyUsage", "GenerateCSR");
380                 return;
381             }
382         }
383         else if (certURI.starts_with(
384                      "/redfish/v1/AccountService/LDAP/Certificates"))
385         {
386             if (optKeyUsage->empty())
387             {
388                 optKeyUsage->push_back("ClientAuthentication");
389             }
390             else if (optKeyUsage->size() == 1)
391             {
392                 if ((*optKeyUsage)[0] != "ClientAuthentication")
393                 {
394                     messages::propertyValueNotInList(
395                         asyncResp->res, (*optKeyUsage)[0], "KeyUsage");
396                     return;
397                 }
398             }
399             else
400             {
401                 messages::actionParameterNotSupported(
402                     asyncResp->res, "KeyUsage", "GenerateCSR");
403                 return;
404             }
405         }
406 
407         // Only allow one CSR matcher at a time so setting retry
408         // time-out and timer expiry to 10 seconds for now.
409         static const int timeOut = 10;
410         if (csrMatcher)
411         {
412             messages::serviceTemporarilyUnavailable(asyncResp->res,
413                                                     std::to_string(timeOut));
414             return;
415         }
416 
417         // Make this static so it survives outside this method
418         static boost::asio::steady_timer timeout(*req.ioService);
419         timeout.expires_after(std::chrono::seconds(timeOut));
420         timeout.async_wait([asyncResp](const boost::system::error_code& ec) {
421             csrMatcher = nullptr;
422             if (ec)
423             {
424                 // operation_aborted is expected if timer is canceled
425                 // before completion.
426                 if (ec != boost::asio::error::operation_aborted)
427                 {
428                     BMCWEB_LOG_ERROR << "Async_wait failed " << ec;
429                 }
430                 return;
431             }
432             BMCWEB_LOG_ERROR << "Timed out waiting for Generating CSR";
433             messages::internalError(asyncResp->res);
434         });
435 
436         // create a matcher to wait on CSR object
437         BMCWEB_LOG_DEBUG << "create matcher with path " << objectPath;
438         std::string match("type='signal',"
439                           "interface='org.freedesktop.DBus.ObjectManager',"
440                           "path='" +
441                           objectPath +
442                           "',"
443                           "member='InterfacesAdded'");
444         csrMatcher = std::make_unique<sdbusplus::bus::match::match>(
445             *crow::connections::systemBus, match,
446             [asyncResp, service, objectPath,
447              certURI](sdbusplus::message::message& m) {
448             timeout.cancel();
449             if (m.is_method_error())
450             {
451                 BMCWEB_LOG_ERROR << "Dbus method error!!!";
452                 messages::internalError(asyncResp->res);
453                 return;
454             }
455 
456             dbus::utility::DBusInteracesMap interfacesProperties;
457 
458             sdbusplus::message::object_path csrObjectPath;
459             m.read(csrObjectPath, interfacesProperties);
460             BMCWEB_LOG_DEBUG << "CSR object added" << csrObjectPath.str;
461             for (const auto& interface : interfacesProperties)
462             {
463                 if (interface.first == "xyz.openbmc_project.Certs.CSR")
464                 {
465                     getCSR(asyncResp, certURI, service, objectPath,
466                            csrObjectPath.str);
467                     break;
468                 }
469             }
470             });
471         crow::connections::systemBus->async_method_call(
472             [asyncResp](const boost::system::error_code ec,
473                         const std::string&) {
474             if (ec)
475             {
476                 BMCWEB_LOG_ERROR << "DBUS response error: " << ec.message();
477                 messages::internalError(asyncResp->res);
478                 return;
479             }
480             },
481             service, objectPath, "xyz.openbmc_project.Certs.CSR.Create",
482             "GenerateCSR", *optAlternativeNames, *optChallengePassword, city,
483             commonName, *optContactPerson, country, *optEmail, *optGivenName,
484             *optInitials, *optKeyBitLength, *optKeyCurveId,
485             *optKeyPairAlgorithm, *optKeyUsage, organization,
486             organizationalUnit, state, *optSurname, *optUnstructuredName);
487         });
488 } // requestRoutesCertificateActionGenerateCSR
489 
490 /**
491  * @brief Parse and update Certificate Issue/Subject property
492  *
493  * @param[in] asyncResp Shared pointer to the response message
494  * @param[in] str  Issuer/Subject value in key=value pairs
495  * @param[in] type Issuer/Subject
496  * @return None
497  */
498 static void updateCertIssuerOrSubject(nlohmann::json& out,
499                                       const std::string_view value)
500 {
501     // example: O=openbmc-project.xyz,CN=localhost
502     std::string_view::iterator i = value.begin();
503     while (i != value.end())
504     {
505         std::string_view::iterator tokenBegin = i;
506         while (i != value.end() && *i != '=')
507         {
508             ++i;
509         }
510         if (i == value.end())
511         {
512             break;
513         }
514         const std::string_view key(tokenBegin,
515                                    static_cast<size_t>(i - tokenBegin));
516         ++i;
517         tokenBegin = i;
518         while (i != value.end() && *i != ',')
519         {
520             ++i;
521         }
522         const std::string_view val(tokenBegin,
523                                    static_cast<size_t>(i - tokenBegin));
524         if (key == "L")
525         {
526             out["City"] = val;
527         }
528         else if (key == "CN")
529         {
530             out["CommonName"] = val;
531         }
532         else if (key == "C")
533         {
534             out["Country"] = val;
535         }
536         else if (key == "O")
537         {
538             out["Organization"] = val;
539         }
540         else if (key == "OU")
541         {
542             out["OrganizationalUnit"] = val;
543         }
544         else if (key == "ST")
545         {
546             out["State"] = val;
547         }
548         // skip comma character
549         if (i != value.end())
550         {
551             ++i;
552         }
553     }
554 }
555 
556 /**
557  * @brief Retrieve the installed certificate list
558  *
559  * @param[in] asyncResp Shared pointer to the response message
560  * @param[in] basePath DBus object path to search
561  * @param[in] listPtr Json pointer to the list in asyncResp
562  * @param[in] countPtr Json pointer to the count in asyncResp
563  * @return None
564  */
565 static void
566     getCertificateList(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
567                        const std::string& basePath,
568                        const nlohmann::json::json_pointer& listPtr,
569                        const nlohmann::json::json_pointer& countPtr)
570 {
571     crow::connections::systemBus->async_method_call(
572         [asyncResp, listPtr, countPtr](
573             const boost::system::error_code ec,
574             const dbus::utility::MapperGetSubTreePathsResponse& certPaths) {
575         if (ec)
576         {
577             BMCWEB_LOG_ERROR << "Certificate collection query failed: " << ec;
578             messages::internalError(asyncResp->res);
579             return;
580         }
581 
582         nlohmann::json& links = asyncResp->res.jsonValue[listPtr];
583         links = nlohmann::json::array();
584         for (const auto& certPath : certPaths)
585         {
586             sdbusplus::message::object_path objPath(certPath);
587             std::string certId = objPath.filename();
588             if (certId.empty())
589             {
590                 BMCWEB_LOG_ERROR << "Invalid certificate objPath " << certPath;
591                 continue;
592             }
593 
594             boost::urls::url certURL;
595             if (objPath.parent_path() == certs::httpsObjectPath)
596             {
597                 certURL = crow::utility::urlFromPieces(
598                     "redfish", "v1", "Managers", "bmc", "NetworkProtocol",
599                     "HTTPS", "Certificates", certId);
600             }
601             else if (objPath.parent_path() == certs::ldapObjectPath)
602             {
603                 certURL = crow::utility::urlFromPieces("redfish", "v1",
604                                                        "AccountService", "LDAP",
605                                                        "Certificates", certId);
606             }
607             else if (objPath.parent_path() == certs::authorityObjectPath)
608             {
609                 certURL = crow::utility::urlFromPieces(
610                     "redfish", "v1", "Managers", "bmc", "Truststore",
611                     "Certificates", certId);
612             }
613             else
614             {
615                 continue;
616             }
617 
618             nlohmann::json::object_t link;
619             link["@odata.id"] = certURL;
620             links.emplace_back(std::move(link));
621         }
622 
623         asyncResp->res.jsonValue[countPtr] = links.size();
624         },
625         "xyz.openbmc_project.ObjectMapper",
626         "/xyz/openbmc_project/object_mapper",
627         "xyz.openbmc_project.ObjectMapper", "GetSubTreePaths", basePath, 0,
628         std::array<const char*, 1>{certs::certPropIntf});
629 }
630 
631 /**
632  * @brief Retrieve the certificates properties and append to the response
633  * message
634  *
635  * @param[in] asyncResp Shared pointer to the response message
636  * @param[in] objectPath  Path of the D-Bus service object
637  * @param[in] certId  Id of the certificate
638  * @param[in] certURL  URL of the certificate object
639  * @param[in] name  name of the certificate
640  * @return None
641  */
642 static void getCertificateProperties(
643     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
644     const std::string& objectPath, const std::string& service, long certId,
645     const std::string& certURL, const std::string& name)
646 {
647     BMCWEB_LOG_DEBUG << "getCertificateProperties Path=" << objectPath
648                      << " certId=" << certId << " certURl=" << certURL;
649     crow::connections::systemBus->async_method_call(
650         [asyncResp, certURL, certId,
651          name](const boost::system::error_code ec,
652                const dbus::utility::DBusPropertiesMap& properties) {
653         if (ec)
654         {
655             BMCWEB_LOG_ERROR << "DBUS response error: " << ec;
656             messages::resourceNotFound(asyncResp->res, name,
657                                        std::to_string(certId));
658             return;
659         }
660         asyncResp->res.jsonValue["@odata.id"] = certURL;
661         asyncResp->res.jsonValue["@odata.type"] =
662             "#Certificate.v1_0_0.Certificate";
663         asyncResp->res.jsonValue["Id"] = std::to_string(certId);
664         asyncResp->res.jsonValue["Name"] = name;
665         asyncResp->res.jsonValue["Description"] = name;
666         for (const auto& property : properties)
667         {
668             if (property.first == "CertificateString")
669             {
670                 asyncResp->res.jsonValue["CertificateString"] = "";
671                 const std::string* value =
672                     std::get_if<std::string>(&property.second);
673                 if (value != nullptr)
674                 {
675                     asyncResp->res.jsonValue["CertificateString"] = *value;
676                 }
677             }
678             else if (property.first == "KeyUsage")
679             {
680                 nlohmann::json& keyUsage = asyncResp->res.jsonValue["KeyUsage"];
681                 keyUsage = nlohmann::json::array();
682                 const std::vector<std::string>* value =
683                     std::get_if<std::vector<std::string>>(&property.second);
684                 if (value != nullptr)
685                 {
686                     for (const std::string& usage : *value)
687                     {
688                         keyUsage.push_back(usage);
689                     }
690                 }
691             }
692             else if (property.first == "Issuer")
693             {
694                 const std::string* value =
695                     std::get_if<std::string>(&property.second);
696                 if (value != nullptr)
697                 {
698                     updateCertIssuerOrSubject(
699                         asyncResp->res.jsonValue["Issuer"], *value);
700                 }
701             }
702             else if (property.first == "Subject")
703             {
704                 const std::string* value =
705                     std::get_if<std::string>(&property.second);
706                 if (value != nullptr)
707                 {
708                     updateCertIssuerOrSubject(
709                         asyncResp->res.jsonValue["Subject"], *value);
710                 }
711             }
712             else if (property.first == "ValidNotAfter")
713             {
714                 const uint64_t* value = std::get_if<uint64_t>(&property.second);
715                 if (value != nullptr)
716                 {
717                     asyncResp->res.jsonValue["ValidNotAfter"] =
718                         crow::utility::getDateTimeUint(*value);
719                 }
720             }
721             else if (property.first == "ValidNotBefore")
722             {
723                 const uint64_t* value = std::get_if<uint64_t>(&property.second);
724                 if (value != nullptr)
725                 {
726                     asyncResp->res.jsonValue["ValidNotBefore"] =
727                         crow::utility::getDateTimeUint(*value);
728                 }
729             }
730         }
731         asyncResp->res.addHeader("Location", certURL);
732         },
733         service, objectPath, certs::dbusPropIntf, "GetAll",
734         certs::certPropIntf);
735 }
736 
737 /**
738  * Action to replace an existing certificate
739  */
740 inline void requestRoutesCertificateActionsReplaceCertificate(App& app)
741 {
742     BMCWEB_ROUTE(
743         app,
744         "/redfish/v1/CertificateService/Actions/CertificateService.ReplaceCertificate/")
745         .privileges(redfish::privileges::postCertificateService)
746         .methods(boost::beast::http::verb::post)(
747             [&app](const crow::Request& req,
748                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
749         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
750         {
751             return;
752         }
753         std::string certificate;
754         nlohmann::json certificateUri;
755         std::optional<std::string> certificateType = "PEM";
756 
757         if (!json_util::readJsonAction(req, asyncResp->res, "CertificateString",
758                                        certificate, "CertificateUri",
759                                        certificateUri, "CertificateType",
760                                        certificateType))
761         {
762             BMCWEB_LOG_ERROR << "Required parameters are missing";
763             messages::internalError(asyncResp->res);
764             return;
765         }
766 
767         if (!certificateType)
768         {
769             // should never happen, but it never hurts to be paranoid.
770             return;
771         }
772         if (certificateType != "PEM")
773         {
774             messages::actionParameterNotSupported(
775                 asyncResp->res, "CertificateType", "ReplaceCertificate");
776             return;
777         }
778 
779         std::string certURI;
780         if (!redfish::json_util::readJson(certificateUri, asyncResp->res,
781                                           "@odata.id", certURI))
782         {
783             messages::actionParameterMissing(
784                 asyncResp->res, "ReplaceCertificate", "CertificateUri");
785             return;
786         }
787 
788         BMCWEB_LOG_INFO << "Certificate URI to replace" << certURI;
789         long id = getIDFromURL(certURI);
790         if (id < 0)
791         {
792             messages::actionParameterValueFormatError(asyncResp->res, certURI,
793                                                       "CertificateUri",
794                                                       "ReplaceCertificate");
795             return;
796         }
797         std::string objectPath;
798         std::string name;
799         std::string service;
800         if (certURI.starts_with(
801                 "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/"))
802         {
803             objectPath =
804                 std::string(certs::httpsObjectPath) + "/" + std::to_string(id);
805             name = "HTTPS certificate";
806             service = certs::httpsServiceName;
807         }
808         else if (certURI.starts_with(
809                      "/redfish/v1/AccountService/LDAP/Certificates/"))
810         {
811             objectPath =
812                 std::string(certs::ldapObjectPath) + "/" + std::to_string(id);
813             name = "LDAP certificate";
814             service = certs::ldapServiceName;
815         }
816         else if (certURI.starts_with(
817                      "/redfish/v1/Managers/bmc/Truststore/Certificates/"))
818         {
819             objectPath = std::string(certs::authorityObjectPath) + "/" +
820                          std::to_string(id);
821             name = "TrustStore certificate";
822             service = certs::authorityServiceName;
823         }
824         else
825         {
826             messages::actionParameterNotSupported(
827                 asyncResp->res, "CertificateUri", "ReplaceCertificate");
828             return;
829         }
830 
831         std::shared_ptr<CertificateFile> certFile =
832             std::make_shared<CertificateFile>(certificate);
833         crow::connections::systemBus->async_method_call(
834             [asyncResp, certFile, objectPath, service, certURI, id,
835              name](const boost::system::error_code ec) {
836             if (ec)
837             {
838                 BMCWEB_LOG_ERROR << "DBUS response error: " << ec;
839                 if (ec.value() ==
840                     boost::system::linux_error::bad_request_descriptor)
841                 {
842                     messages::resourceNotFound(asyncResp->res, name,
843                                                std::to_string(id));
844                     return;
845                 }
846                 messages::internalError(asyncResp->res);
847                 return;
848             }
849             getCertificateProperties(asyncResp, objectPath, service, id,
850                                      certURI, name);
851             BMCWEB_LOG_DEBUG << "HTTPS certificate install file="
852                              << certFile->getCertFilePath();
853             },
854             service, objectPath, certs::certReplaceIntf, "Replace",
855             certFile->getCertFilePath());
856         });
857 } // requestRoutesCertificateActionsReplaceCertificate
858 
859 /**
860  * Certificate resource describes a certificate used to prove the identity
861  * of a component, account or service.
862  */
863 
864 inline void requestRoutesHTTPSCertificate(App& app)
865 {
866     BMCWEB_ROUTE(
867         app,
868         "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/<str>/")
869         .privileges(redfish::privileges::getCertificate)
870         .methods(
871             boost::beast::http::verb::
872                 get)([&app](const crow::Request& req,
873                             const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
874                             const std::string& param) -> void {
875             if (!redfish::setUpRedfishRoute(app, req, asyncResp))
876             {
877                 return;
878             }
879             if (param.empty())
880             {
881                 messages::internalError(asyncResp->res);
882                 return;
883             }
884             long id = getIDFromURL(req.url);
885 
886             BMCWEB_LOG_DEBUG << "HTTPSCertificate::doGet ID="
887                              << std::to_string(id);
888             std::string certURL =
889                 "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/" +
890                 std::to_string(id);
891             std::string objectPath = certs::httpsObjectPath;
892             objectPath += "/";
893             objectPath += std::to_string(id);
894             getCertificateProperties(asyncResp, objectPath,
895                                      certs::httpsServiceName, id, certURL,
896                                      "HTTPS Certificate");
897         });
898 }
899 
900 /**
901  * Collection of HTTPS certificates
902  */
903 inline void requestRoutesHTTPSCertificateCollection(App& app)
904 {
905     BMCWEB_ROUTE(app,
906                  "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/")
907         .privileges(redfish::privileges::getCertificateCollection)
908         .methods(boost::beast::http::verb::get)(
909             [&app](const crow::Request& req,
910                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
911         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
912         {
913             return;
914         }
915 
916         asyncResp->res.jsonValue["@odata.id"] =
917             "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates";
918         asyncResp->res.jsonValue["@odata.type"] =
919             "#CertificateCollection.CertificateCollection";
920         asyncResp->res.jsonValue["Name"] = "HTTPS Certificates Collection";
921         asyncResp->res.jsonValue["Description"] =
922             "A Collection of HTTPS certificate instances";
923 
924         getCertificateList(asyncResp, certs::httpsObjectPath,
925                            "/Members"_json_pointer,
926                            "/Members@odata.count"_json_pointer);
927         });
928 
929     BMCWEB_ROUTE(app,
930                  "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/")
931         .privileges(redfish::privileges::postCertificateCollection)
932         .methods(boost::beast::http::verb::post)(
933             [&app](const crow::Request& req,
934                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
935         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
936         {
937             return;
938         }
939         BMCWEB_LOG_DEBUG << "HTTPSCertificateCollection::doPost";
940 
941         asyncResp->res.jsonValue["Name"] = "HTTPS Certificate";
942         asyncResp->res.jsonValue["Description"] = "HTTPS Certificate";
943 
944         std::string certFileBody = getCertificateFromReqBody(asyncResp, req);
945 
946         if (certFileBody.empty())
947         {
948             BMCWEB_LOG_ERROR << "Cannot get certificate from request body.";
949             messages::unrecognizedRequestBody(asyncResp->res);
950             return;
951         }
952 
953         std::shared_ptr<CertificateFile> certFile =
954             std::make_shared<CertificateFile>(certFileBody);
955 
956         crow::connections::systemBus->async_method_call(
957             [asyncResp, certFile](const boost::system::error_code ec,
958                                   const std::string& objectPath) {
959             if (ec)
960             {
961                 BMCWEB_LOG_ERROR << "DBUS response error: " << ec;
962                 messages::internalError(asyncResp->res);
963                 return;
964             }
965             long certId = getIDFromURL(objectPath);
966             if (certId < 0)
967             {
968                 BMCWEB_LOG_ERROR << "Invalid objectPath value" << objectPath;
969                 messages::internalError(asyncResp->res);
970                 return;
971             }
972             std::string certURL =
973                 "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/" +
974                 std::to_string(certId);
975             getCertificateProperties(asyncResp, objectPath,
976                                      certs::httpsServiceName, certId, certURL,
977                                      "HTTPS Certificate");
978             BMCWEB_LOG_DEBUG << "HTTPS certificate install file="
979                              << certFile->getCertFilePath();
980             },
981             certs::httpsServiceName, certs::httpsObjectPath,
982             certs::certInstallIntf, "Install", certFile->getCertFilePath());
983         });
984 } // requestRoutesHTTPSCertificateCollection
985 
986 /**
987  * The certificate location schema defines a resource that an administrator
988  * can use in order to locate all certificates installed on a given service.
989  */
990 inline void requestRoutesCertificateLocations(App& app)
991 {
992     BMCWEB_ROUTE(app, "/redfish/v1/CertificateService/CertificateLocations/")
993         .privileges(redfish::privileges::getCertificateLocations)
994         .methods(boost::beast::http::verb::get)(
995             [&app](const crow::Request& req,
996                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
997         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
998         {
999             return;
1000         }
1001         asyncResp->res.jsonValue["@odata.id"] =
1002             "/redfish/v1/CertificateService/CertificateLocations";
1003         asyncResp->res.jsonValue["@odata.type"] =
1004             "#CertificateLocations.v1_0_0.CertificateLocations";
1005         asyncResp->res.jsonValue["Name"] = "Certificate Locations";
1006         asyncResp->res.jsonValue["Id"] = "CertificateLocations";
1007         asyncResp->res.jsonValue["Description"] =
1008             "Defines a resource that an administrator can use in order to "
1009             "locate all certificates installed on a given service";
1010 
1011         getCertificateList(asyncResp, certs::baseObjectPath,
1012                            "/Links/Certificates"_json_pointer,
1013                            "/Links/Certificates@odata.count"_json_pointer);
1014         });
1015 }
1016 // requestRoutesCertificateLocations
1017 
1018 /**
1019  * Collection of LDAP certificates
1020  */
1021 inline void requestRoutesLDAPCertificateCollection(App& app)
1022 {
1023     BMCWEB_ROUTE(app, "/redfish/v1/AccountService/LDAP/Certificates/")
1024         .privileges(redfish::privileges::getCertificateCollection)
1025         .methods(boost::beast::http::verb::get)(
1026             [&app](const crow::Request& req,
1027                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
1028         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
1029         {
1030             return;
1031         }
1032 
1033         asyncResp->res.jsonValue["@odata.id"] =
1034             "/redfish/v1/AccountService/LDAP/Certificates";
1035         asyncResp->res.jsonValue["@odata.type"] =
1036             "#CertificateCollection.CertificateCollection";
1037         asyncResp->res.jsonValue["Name"] = "LDAP Certificates Collection";
1038         asyncResp->res.jsonValue["Description"] =
1039             "A Collection of LDAP certificate instances";
1040 
1041         getCertificateList(asyncResp, certs::ldapObjectPath,
1042                            "/Members"_json_pointer,
1043                            "/Members@odata.count"_json_pointer);
1044         });
1045 
1046     BMCWEB_ROUTE(app, "/redfish/v1/AccountService/LDAP/Certificates/")
1047         .privileges(redfish::privileges::postCertificateCollection)
1048         .methods(boost::beast::http::verb::post)(
1049             [&app](const crow::Request& req,
1050                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
1051         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
1052         {
1053             return;
1054         }
1055         std::string certFileBody = getCertificateFromReqBody(asyncResp, req);
1056 
1057         if (certFileBody.empty())
1058         {
1059             BMCWEB_LOG_ERROR << "Cannot get certificate from request body.";
1060             messages::unrecognizedRequestBody(asyncResp->res);
1061             return;
1062         }
1063 
1064         std::shared_ptr<CertificateFile> certFile =
1065             std::make_shared<CertificateFile>(certFileBody);
1066 
1067         crow::connections::systemBus->async_method_call(
1068             [asyncResp, certFile](const boost::system::error_code ec,
1069                                   const std::string& objectPath) {
1070             if (ec)
1071             {
1072                 BMCWEB_LOG_ERROR << "DBUS response error: " << ec;
1073                 messages::internalError(asyncResp->res);
1074                 return;
1075             }
1076             long certId = getIDFromURL(objectPath);
1077             if (certId < 0)
1078             {
1079                 BMCWEB_LOG_ERROR << "Invalid objectPath value" << objectPath;
1080                 messages::internalError(asyncResp->res);
1081                 return;
1082             }
1083             std::string certURL =
1084                 "/redfish/v1/AccountService/LDAP/Certificates/" +
1085                 std::to_string(certId);
1086             getCertificateProperties(asyncResp, objectPath,
1087                                      certs::ldapServiceName, certId, certURL,
1088                                      "LDAP Certificate");
1089             BMCWEB_LOG_DEBUG << "LDAP certificate install file="
1090                              << certFile->getCertFilePath();
1091             },
1092             certs::ldapServiceName, certs::ldapObjectPath,
1093             certs::certInstallIntf, "Install", certFile->getCertFilePath());
1094         });
1095 } // requestRoutesLDAPCertificateCollection
1096 
1097 /**
1098  * Certificate resource describes a certificate used to prove the identity
1099  * of a component, account or service.
1100  */
1101 inline void requestRoutesLDAPCertificate(App& app)
1102 {
1103     BMCWEB_ROUTE(app, "/redfish/v1/AccountService/LDAP/Certificates/<str>/")
1104         .privileges(redfish::privileges::getCertificate)
1105         .methods(boost::beast::http::verb::get)(
1106             [&app](const crow::Request& req,
1107                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1108                    const std::string&) {
1109         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
1110         {
1111             return;
1112         }
1113         long id = getIDFromURL(req.url);
1114         if (id < 0)
1115         {
1116             BMCWEB_LOG_ERROR << "Invalid url value" << req.url;
1117             messages::internalError(asyncResp->res);
1118             return;
1119         }
1120         BMCWEB_LOG_DEBUG << "LDAP Certificate ID=" << std::to_string(id);
1121         std::string certURL = "/redfish/v1/AccountService/LDAP/Certificates/" +
1122                               std::to_string(id);
1123         std::string objectPath = certs::ldapObjectPath;
1124         objectPath += "/";
1125         objectPath += std::to_string(id);
1126         getCertificateProperties(asyncResp, objectPath, certs::ldapServiceName,
1127                                  id, certURL, "LDAP Certificate");
1128         });
1129 } // requestRoutesLDAPCertificate
1130 /**
1131  * Collection of TrustStoreCertificate certificates
1132  */
1133 inline void requestRoutesTrustStoreCertificateCollection(App& app)
1134 {
1135     BMCWEB_ROUTE(app, "/redfish/v1/Managers/bmc/Truststore/Certificates/")
1136         .privileges(redfish::privileges::getCertificate)
1137         .methods(boost::beast::http::verb::get)(
1138             [&app](const crow::Request& req,
1139                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
1140         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
1141         {
1142             return;
1143         }
1144 
1145         asyncResp->res.jsonValue["@odata.id"] =
1146             "/redfish/v1/Managers/bmc/Truststore/Certificates/";
1147         asyncResp->res.jsonValue["@odata.type"] =
1148             "#CertificateCollection.CertificateCollection";
1149         asyncResp->res.jsonValue["Name"] = "TrustStore Certificates Collection";
1150         asyncResp->res.jsonValue["Description"] =
1151             "A Collection of TrustStore certificate instances";
1152 
1153         getCertificateList(asyncResp, certs::authorityObjectPath,
1154                            "/Members"_json_pointer,
1155                            "/Members@odata.count"_json_pointer);
1156         });
1157 
1158     BMCWEB_ROUTE(app, "/redfish/v1/Managers/bmc/Truststore/Certificates/")
1159         .privileges(redfish::privileges::postCertificateCollection)
1160         .methods(boost::beast::http::verb::post)(
1161             [&app](const crow::Request& req,
1162                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
1163         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
1164         {
1165             return;
1166         }
1167         std::string certFileBody = getCertificateFromReqBody(asyncResp, req);
1168 
1169         if (certFileBody.empty())
1170         {
1171             BMCWEB_LOG_ERROR << "Cannot get certificate from request body.";
1172             messages::unrecognizedRequestBody(asyncResp->res);
1173             return;
1174         }
1175 
1176         std::shared_ptr<CertificateFile> certFile =
1177             std::make_shared<CertificateFile>(certFileBody);
1178         crow::connections::systemBus->async_method_call(
1179             [asyncResp, certFile](const boost::system::error_code ec,
1180                                   const std::string& objectPath) {
1181             if (ec)
1182             {
1183                 BMCWEB_LOG_ERROR << "DBUS response error: " << ec;
1184                 messages::internalError(asyncResp->res);
1185                 return;
1186             }
1187             long certId = getIDFromURL(objectPath);
1188             if (certId < 0)
1189             {
1190                 BMCWEB_LOG_ERROR << "Invalid objectPath value" << objectPath;
1191                 messages::internalError(asyncResp->res);
1192                 return;
1193             }
1194             std::string certURL =
1195                 "/redfish/v1/Managers/bmc/Truststore/Certificates/" +
1196                 std::to_string(certId);
1197 
1198             getCertificateProperties(asyncResp, objectPath,
1199                                      certs::authorityServiceName, certId,
1200                                      certURL, "TrustStore Certificate");
1201             BMCWEB_LOG_DEBUG << "TrustStore certificate install file="
1202                              << certFile->getCertFilePath();
1203             },
1204             certs::authorityServiceName, certs::authorityObjectPath,
1205             certs::certInstallIntf, "Install", certFile->getCertFilePath());
1206         });
1207 } // requestRoutesTrustStoreCertificateCollection
1208 
1209 /**
1210  * Certificate resource describes a certificate used to prove the identity
1211  * of a component, account or service.
1212  */
1213 inline void requestRoutesTrustStoreCertificate(App& app)
1214 {
1215     BMCWEB_ROUTE(app, "/redfish/v1/Managers/bmc/Truststore/Certificates/<str>/")
1216         .privileges(redfish::privileges::getCertificate)
1217         .methods(boost::beast::http::verb::get)(
1218             [&app](const crow::Request& req,
1219                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1220                    const std::string&) {
1221         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
1222         {
1223             return;
1224         }
1225         long id = getIDFromURL(req.url);
1226         if (id < 0)
1227         {
1228             BMCWEB_LOG_ERROR << "Invalid url value" << req.url;
1229             messages::internalError(asyncResp->res);
1230             return;
1231         }
1232         BMCWEB_LOG_DEBUG << "TrustStoreCertificate::doGet ID="
1233                          << std::to_string(id);
1234         std::string certURL =
1235             "/redfish/v1/Managers/bmc/Truststore/Certificates/" +
1236             std::to_string(id);
1237         std::string objectPath = certs::authorityObjectPath;
1238         objectPath += "/";
1239         objectPath += std::to_string(id);
1240         getCertificateProperties(asyncResp, objectPath,
1241                                  certs::authorityServiceName, id, certURL,
1242                                  "TrustStore Certificate");
1243         });
1244 
1245     BMCWEB_ROUTE(app, "/redfish/v1/Managers/bmc/Truststore/Certificates/<str>/")
1246         .privileges(redfish::privileges::deleteCertificate)
1247         .methods(boost::beast::http::verb::delete_)(
1248             [&app](const crow::Request& req,
1249                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1250                    const std::string& param) {
1251         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
1252         {
1253             return;
1254         }
1255         if (param.empty())
1256         {
1257             messages::internalError(asyncResp->res);
1258             return;
1259         }
1260 
1261         long id = getIDFromURL(req.url);
1262         if (id < 0)
1263         {
1264             BMCWEB_LOG_ERROR << "Invalid url value: " << req.url;
1265             messages::resourceNotFound(asyncResp->res, "TrustStore Certificate",
1266                                        std::string(req.url));
1267             return;
1268         }
1269         BMCWEB_LOG_DEBUG << "TrustStoreCertificate::doDelete ID="
1270                          << std::to_string(id);
1271         std::string certPath = certs::authorityObjectPath;
1272         certPath += "/";
1273         certPath += std::to_string(id);
1274 
1275         crow::connections::systemBus->async_method_call(
1276             [asyncResp, id](const boost::system::error_code ec) {
1277             if (ec)
1278             {
1279                 messages::resourceNotFound(asyncResp->res,
1280                                            "TrustStore Certificate",
1281                                            std::to_string(id));
1282                 return;
1283             }
1284             BMCWEB_LOG_INFO << "Certificate deleted";
1285             asyncResp->res.result(boost::beast::http::status::no_content);
1286             },
1287             certs::authorityServiceName, certPath, certs::objDeleteIntf,
1288             "Delete");
1289         });
1290 } // requestRoutesTrustStoreCertificate
1291 } // namespace redfish
1292