xref: /openbmc/bmcweb/features/redfish/lib/certificate_service.hpp (revision 59d494ee9608850f17bd7f4644838c26daab2669)
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_t> 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_t>(
445             *crow::connections::systemBus, match,
446             [asyncResp, service, objectPath, certURI](sdbusplus::message_t& m) {
447             timeout.cancel();
448             if (m.is_method_error())
449             {
450                 BMCWEB_LOG_ERROR << "Dbus method error!!!";
451                 messages::internalError(asyncResp->res);
452                 return;
453             }
454 
455             dbus::utility::DBusInteracesMap interfacesProperties;
456 
457             sdbusplus::message::object_path csrObjectPath;
458             m.read(csrObjectPath, interfacesProperties);
459             BMCWEB_LOG_DEBUG << "CSR object added" << csrObjectPath.str;
460             for (const auto& interface : interfacesProperties)
461             {
462                 if (interface.first == "xyz.openbmc_project.Certs.CSR")
463                 {
464                     getCSR(asyncResp, certURI, service, objectPath,
465                            csrObjectPath.str);
466                     break;
467                 }
468             }
469             });
470         crow::connections::systemBus->async_method_call(
471             [asyncResp](const boost::system::error_code ec,
472                         const std::string&) {
473             if (ec)
474             {
475                 BMCWEB_LOG_ERROR << "DBUS response error: " << ec.message();
476                 messages::internalError(asyncResp->res);
477                 return;
478             }
479             },
480             service, objectPath, "xyz.openbmc_project.Certs.CSR.Create",
481             "GenerateCSR", *optAlternativeNames, *optChallengePassword, city,
482             commonName, *optContactPerson, country, *optEmail, *optGivenName,
483             *optInitials, *optKeyBitLength, *optKeyCurveId,
484             *optKeyPairAlgorithm, *optKeyUsage, organization,
485             organizationalUnit, state, *optSurname, *optUnstructuredName);
486         });
487 } // requestRoutesCertificateActionGenerateCSR
488 
489 /**
490  * @brief Parse and update Certificate Issue/Subject property
491  *
492  * @param[in] asyncResp Shared pointer to the response message
493  * @param[in] str  Issuer/Subject value in key=value pairs
494  * @param[in] type Issuer/Subject
495  * @return None
496  */
497 static void updateCertIssuerOrSubject(nlohmann::json& out,
498                                       const std::string_view value)
499 {
500     // example: O=openbmc-project.xyz,CN=localhost
501     std::string_view::iterator i = value.begin();
502     while (i != value.end())
503     {
504         std::string_view::iterator tokenBegin = i;
505         while (i != value.end() && *i != '=')
506         {
507             ++i;
508         }
509         if (i == value.end())
510         {
511             break;
512         }
513         const std::string_view key(tokenBegin,
514                                    static_cast<size_t>(i - tokenBegin));
515         ++i;
516         tokenBegin = i;
517         while (i != value.end() && *i != ',')
518         {
519             ++i;
520         }
521         const std::string_view val(tokenBegin,
522                                    static_cast<size_t>(i - tokenBegin));
523         if (key == "L")
524         {
525             out["City"] = val;
526         }
527         else if (key == "CN")
528         {
529             out["CommonName"] = val;
530         }
531         else if (key == "C")
532         {
533             out["Country"] = val;
534         }
535         else if (key == "O")
536         {
537             out["Organization"] = val;
538         }
539         else if (key == "OU")
540         {
541             out["OrganizationalUnit"] = val;
542         }
543         else if (key == "ST")
544         {
545             out["State"] = val;
546         }
547         // skip comma character
548         if (i != value.end())
549         {
550             ++i;
551         }
552     }
553 }
554 
555 /**
556  * @brief Retrieve the installed certificate list
557  *
558  * @param[in] asyncResp Shared pointer to the response message
559  * @param[in] basePath DBus object path to search
560  * @param[in] listPtr Json pointer to the list in asyncResp
561  * @param[in] countPtr Json pointer to the count in asyncResp
562  * @return None
563  */
564 static void
565     getCertificateList(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
566                        const std::string& basePath,
567                        const nlohmann::json::json_pointer& listPtr,
568                        const nlohmann::json::json_pointer& countPtr)
569 {
570     crow::connections::systemBus->async_method_call(
571         [asyncResp, listPtr, countPtr](
572             const boost::system::error_code ec,
573             const dbus::utility::MapperGetSubTreePathsResponse& certPaths) {
574         if (ec)
575         {
576             BMCWEB_LOG_ERROR << "Certificate collection query failed: " << ec;
577             messages::internalError(asyncResp->res);
578             return;
579         }
580 
581         nlohmann::json& links = asyncResp->res.jsonValue[listPtr];
582         links = nlohmann::json::array();
583         for (const auto& certPath : certPaths)
584         {
585             sdbusplus::message::object_path objPath(certPath);
586             std::string certId = objPath.filename();
587             if (certId.empty())
588             {
589                 BMCWEB_LOG_ERROR << "Invalid certificate objPath " << certPath;
590                 continue;
591             }
592 
593             boost::urls::url certURL;
594             if (objPath.parent_path() == certs::httpsObjectPath)
595             {
596                 certURL = crow::utility::urlFromPieces(
597                     "redfish", "v1", "Managers", "bmc", "NetworkProtocol",
598                     "HTTPS", "Certificates", certId);
599             }
600             else if (objPath.parent_path() == certs::ldapObjectPath)
601             {
602                 certURL = crow::utility::urlFromPieces("redfish", "v1",
603                                                        "AccountService", "LDAP",
604                                                        "Certificates", certId);
605             }
606             else if (objPath.parent_path() == certs::authorityObjectPath)
607             {
608                 certURL = crow::utility::urlFromPieces(
609                     "redfish", "v1", "Managers", "bmc", "Truststore",
610                     "Certificates", certId);
611             }
612             else
613             {
614                 continue;
615             }
616 
617             nlohmann::json::object_t link;
618             link["@odata.id"] = certURL;
619             links.emplace_back(std::move(link));
620         }
621 
622         asyncResp->res.jsonValue[countPtr] = links.size();
623         },
624         "xyz.openbmc_project.ObjectMapper",
625         "/xyz/openbmc_project/object_mapper",
626         "xyz.openbmc_project.ObjectMapper", "GetSubTreePaths", basePath, 0,
627         std::array<const char*, 1>{certs::certPropIntf});
628 }
629 
630 /**
631  * @brief Retrieve the certificates properties and append to the response
632  * message
633  *
634  * @param[in] asyncResp Shared pointer to the response message
635  * @param[in] objectPath  Path of the D-Bus service object
636  * @param[in] certId  Id of the certificate
637  * @param[in] certURL  URL of the certificate object
638  * @param[in] name  name of the certificate
639  * @return None
640  */
641 static void getCertificateProperties(
642     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
643     const std::string& objectPath, const std::string& service, long certId,
644     const std::string& certURL, const std::string& name)
645 {
646     BMCWEB_LOG_DEBUG << "getCertificateProperties Path=" << objectPath
647                      << " certId=" << certId << " certURl=" << certURL;
648     crow::connections::systemBus->async_method_call(
649         [asyncResp, certURL, certId,
650          name](const boost::system::error_code ec,
651                const dbus::utility::DBusPropertiesMap& properties) {
652         if (ec)
653         {
654             BMCWEB_LOG_ERROR << "DBUS response error: " << ec;
655             messages::resourceNotFound(asyncResp->res, name,
656                                        std::to_string(certId));
657             return;
658         }
659         asyncResp->res.jsonValue["@odata.id"] = certURL;
660         asyncResp->res.jsonValue["@odata.type"] =
661             "#Certificate.v1_0_0.Certificate";
662         asyncResp->res.jsonValue["Id"] = std::to_string(certId);
663         asyncResp->res.jsonValue["Name"] = name;
664         asyncResp->res.jsonValue["Description"] = name;
665         for (const auto& property : properties)
666         {
667             if (property.first == "CertificateString")
668             {
669                 asyncResp->res.jsonValue["CertificateString"] = "";
670                 const std::string* value =
671                     std::get_if<std::string>(&property.second);
672                 if (value != nullptr)
673                 {
674                     asyncResp->res.jsonValue["CertificateString"] = *value;
675                 }
676             }
677             else if (property.first == "KeyUsage")
678             {
679                 nlohmann::json& keyUsage = asyncResp->res.jsonValue["KeyUsage"];
680                 keyUsage = nlohmann::json::array();
681                 const std::vector<std::string>* value =
682                     std::get_if<std::vector<std::string>>(&property.second);
683                 if (value != nullptr)
684                 {
685                     for (const std::string& usage : *value)
686                     {
687                         keyUsage.push_back(usage);
688                     }
689                 }
690             }
691             else if (property.first == "Issuer")
692             {
693                 const std::string* value =
694                     std::get_if<std::string>(&property.second);
695                 if (value != nullptr)
696                 {
697                     updateCertIssuerOrSubject(
698                         asyncResp->res.jsonValue["Issuer"], *value);
699                 }
700             }
701             else if (property.first == "Subject")
702             {
703                 const std::string* value =
704                     std::get_if<std::string>(&property.second);
705                 if (value != nullptr)
706                 {
707                     updateCertIssuerOrSubject(
708                         asyncResp->res.jsonValue["Subject"], *value);
709                 }
710             }
711             else if (property.first == "ValidNotAfter")
712             {
713                 const uint64_t* value = std::get_if<uint64_t>(&property.second);
714                 if (value != nullptr)
715                 {
716                     asyncResp->res.jsonValue["ValidNotAfter"] =
717                         crow::utility::getDateTimeUint(*value);
718                 }
719             }
720             else if (property.first == "ValidNotBefore")
721             {
722                 const uint64_t* value = std::get_if<uint64_t>(&property.second);
723                 if (value != nullptr)
724                 {
725                     asyncResp->res.jsonValue["ValidNotBefore"] =
726                         crow::utility::getDateTimeUint(*value);
727                 }
728             }
729         }
730         asyncResp->res.addHeader("Location", certURL);
731         },
732         service, objectPath, certs::dbusPropIntf, "GetAll",
733         certs::certPropIntf);
734 }
735 
736 /**
737  * Action to replace an existing certificate
738  */
739 inline void requestRoutesCertificateActionsReplaceCertificate(App& app)
740 {
741     BMCWEB_ROUTE(
742         app,
743         "/redfish/v1/CertificateService/Actions/CertificateService.ReplaceCertificate/")
744         .privileges(redfish::privileges::postCertificateService)
745         .methods(boost::beast::http::verb::post)(
746             [&app](const crow::Request& req,
747                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
748         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
749         {
750             return;
751         }
752         std::string certificate;
753         nlohmann::json certificateUri;
754         std::optional<std::string> certificateType = "PEM";
755 
756         if (!json_util::readJsonAction(req, asyncResp->res, "CertificateString",
757                                        certificate, "CertificateUri",
758                                        certificateUri, "CertificateType",
759                                        certificateType))
760         {
761             BMCWEB_LOG_ERROR << "Required parameters are missing";
762             messages::internalError(asyncResp->res);
763             return;
764         }
765 
766         if (!certificateType)
767         {
768             // should never happen, but it never hurts to be paranoid.
769             return;
770         }
771         if (certificateType != "PEM")
772         {
773             messages::actionParameterNotSupported(
774                 asyncResp->res, "CertificateType", "ReplaceCertificate");
775             return;
776         }
777 
778         std::string certURI;
779         if (!redfish::json_util::readJson(certificateUri, asyncResp->res,
780                                           "@odata.id", certURI))
781         {
782             messages::actionParameterMissing(
783                 asyncResp->res, "ReplaceCertificate", "CertificateUri");
784             return;
785         }
786 
787         BMCWEB_LOG_INFO << "Certificate URI to replace" << certURI;
788         long id = getIDFromURL(certURI);
789         if (id < 0)
790         {
791             messages::actionParameterValueFormatError(asyncResp->res, certURI,
792                                                       "CertificateUri",
793                                                       "ReplaceCertificate");
794             return;
795         }
796         std::string objectPath;
797         std::string name;
798         std::string service;
799         if (certURI.starts_with(
800                 "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/"))
801         {
802             objectPath =
803                 std::string(certs::httpsObjectPath) + "/" + std::to_string(id);
804             name = "HTTPS certificate";
805             service = certs::httpsServiceName;
806         }
807         else if (certURI.starts_with(
808                      "/redfish/v1/AccountService/LDAP/Certificates/"))
809         {
810             objectPath =
811                 std::string(certs::ldapObjectPath) + "/" + std::to_string(id);
812             name = "LDAP certificate";
813             service = certs::ldapServiceName;
814         }
815         else if (certURI.starts_with(
816                      "/redfish/v1/Managers/bmc/Truststore/Certificates/"))
817         {
818             objectPath = std::string(certs::authorityObjectPath) + "/" +
819                          std::to_string(id);
820             name = "TrustStore certificate";
821             service = certs::authorityServiceName;
822         }
823         else
824         {
825             messages::actionParameterNotSupported(
826                 asyncResp->res, "CertificateUri", "ReplaceCertificate");
827             return;
828         }
829 
830         std::shared_ptr<CertificateFile> certFile =
831             std::make_shared<CertificateFile>(certificate);
832         crow::connections::systemBus->async_method_call(
833             [asyncResp, certFile, objectPath, service, certURI, id,
834              name](const boost::system::error_code ec) {
835             if (ec)
836             {
837                 BMCWEB_LOG_ERROR << "DBUS response error: " << ec;
838                 if (ec.value() ==
839                     boost::system::linux_error::bad_request_descriptor)
840                 {
841                     messages::resourceNotFound(asyncResp->res, name,
842                                                std::to_string(id));
843                     return;
844                 }
845                 messages::internalError(asyncResp->res);
846                 return;
847             }
848             getCertificateProperties(asyncResp, objectPath, service, id,
849                                      certURI, name);
850             BMCWEB_LOG_DEBUG << "HTTPS certificate install file="
851                              << certFile->getCertFilePath();
852             },
853             service, objectPath, certs::certReplaceIntf, "Replace",
854             certFile->getCertFilePath());
855         });
856 } // requestRoutesCertificateActionsReplaceCertificate
857 
858 /**
859  * Certificate resource describes a certificate used to prove the identity
860  * of a component, account or service.
861  */
862 
863 inline void requestRoutesHTTPSCertificate(App& app)
864 {
865     BMCWEB_ROUTE(
866         app,
867         "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/<str>/")
868         .privileges(redfish::privileges::getCertificate)
869         .methods(
870             boost::beast::http::verb::
871                 get)([&app](const crow::Request& req,
872                             const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
873                             const std::string& param) -> void {
874             if (!redfish::setUpRedfishRoute(app, req, asyncResp))
875             {
876                 return;
877             }
878             if (param.empty())
879             {
880                 messages::internalError(asyncResp->res);
881                 return;
882             }
883             long id = getIDFromURL(req.url);
884 
885             BMCWEB_LOG_DEBUG << "HTTPSCertificate::doGet ID="
886                              << std::to_string(id);
887             std::string certURL =
888                 "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/" +
889                 std::to_string(id);
890             std::string objectPath = certs::httpsObjectPath;
891             objectPath += "/";
892             objectPath += std::to_string(id);
893             getCertificateProperties(asyncResp, objectPath,
894                                      certs::httpsServiceName, id, certURL,
895                                      "HTTPS Certificate");
896         });
897 }
898 
899 /**
900  * Collection of HTTPS certificates
901  */
902 inline void requestRoutesHTTPSCertificateCollection(App& app)
903 {
904     BMCWEB_ROUTE(app,
905                  "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/")
906         .privileges(redfish::privileges::getCertificateCollection)
907         .methods(boost::beast::http::verb::get)(
908             [&app](const crow::Request& req,
909                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
910         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
911         {
912             return;
913         }
914 
915         asyncResp->res.jsonValue["@odata.id"] =
916             "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates";
917         asyncResp->res.jsonValue["@odata.type"] =
918             "#CertificateCollection.CertificateCollection";
919         asyncResp->res.jsonValue["Name"] = "HTTPS Certificates Collection";
920         asyncResp->res.jsonValue["Description"] =
921             "A Collection of HTTPS certificate instances";
922 
923         getCertificateList(asyncResp, certs::httpsObjectPath,
924                            "/Members"_json_pointer,
925                            "/Members@odata.count"_json_pointer);
926         });
927 
928     BMCWEB_ROUTE(app,
929                  "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/")
930         .privileges(redfish::privileges::postCertificateCollection)
931         .methods(boost::beast::http::verb::post)(
932             [&app](const crow::Request& req,
933                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
934         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
935         {
936             return;
937         }
938         BMCWEB_LOG_DEBUG << "HTTPSCertificateCollection::doPost";
939 
940         asyncResp->res.jsonValue["Name"] = "HTTPS Certificate";
941         asyncResp->res.jsonValue["Description"] = "HTTPS Certificate";
942 
943         std::string certFileBody = getCertificateFromReqBody(asyncResp, req);
944 
945         if (certFileBody.empty())
946         {
947             BMCWEB_LOG_ERROR << "Cannot get certificate from request body.";
948             messages::unrecognizedRequestBody(asyncResp->res);
949             return;
950         }
951 
952         std::shared_ptr<CertificateFile> certFile =
953             std::make_shared<CertificateFile>(certFileBody);
954 
955         crow::connections::systemBus->async_method_call(
956             [asyncResp, certFile](const boost::system::error_code ec,
957                                   const std::string& objectPath) {
958             if (ec)
959             {
960                 BMCWEB_LOG_ERROR << "DBUS response error: " << ec;
961                 messages::internalError(asyncResp->res);
962                 return;
963             }
964             long certId = getIDFromURL(objectPath);
965             if (certId < 0)
966             {
967                 BMCWEB_LOG_ERROR << "Invalid objectPath value" << objectPath;
968                 messages::internalError(asyncResp->res);
969                 return;
970             }
971             std::string certURL =
972                 "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/" +
973                 std::to_string(certId);
974             getCertificateProperties(asyncResp, objectPath,
975                                      certs::httpsServiceName, certId, certURL,
976                                      "HTTPS Certificate");
977             BMCWEB_LOG_DEBUG << "HTTPS certificate install file="
978                              << certFile->getCertFilePath();
979             },
980             certs::httpsServiceName, certs::httpsObjectPath,
981             certs::certInstallIntf, "Install", certFile->getCertFilePath());
982         });
983 } // requestRoutesHTTPSCertificateCollection
984 
985 /**
986  * The certificate location schema defines a resource that an administrator
987  * can use in order to locate all certificates installed on a given service.
988  */
989 inline void requestRoutesCertificateLocations(App& app)
990 {
991     BMCWEB_ROUTE(app, "/redfish/v1/CertificateService/CertificateLocations/")
992         .privileges(redfish::privileges::getCertificateLocations)
993         .methods(boost::beast::http::verb::get)(
994             [&app](const crow::Request& req,
995                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
996         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
997         {
998             return;
999         }
1000         asyncResp->res.jsonValue["@odata.id"] =
1001             "/redfish/v1/CertificateService/CertificateLocations";
1002         asyncResp->res.jsonValue["@odata.type"] =
1003             "#CertificateLocations.v1_0_0.CertificateLocations";
1004         asyncResp->res.jsonValue["Name"] = "Certificate Locations";
1005         asyncResp->res.jsonValue["Id"] = "CertificateLocations";
1006         asyncResp->res.jsonValue["Description"] =
1007             "Defines a resource that an administrator can use in order to "
1008             "locate all certificates installed on a given service";
1009 
1010         getCertificateList(asyncResp, certs::baseObjectPath,
1011                            "/Links/Certificates"_json_pointer,
1012                            "/Links/Certificates@odata.count"_json_pointer);
1013         });
1014 }
1015 // requestRoutesCertificateLocations
1016 
1017 /**
1018  * Collection of LDAP certificates
1019  */
1020 inline void requestRoutesLDAPCertificateCollection(App& app)
1021 {
1022     BMCWEB_ROUTE(app, "/redfish/v1/AccountService/LDAP/Certificates/")
1023         .privileges(redfish::privileges::getCertificateCollection)
1024         .methods(boost::beast::http::verb::get)(
1025             [&app](const crow::Request& req,
1026                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
1027         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
1028         {
1029             return;
1030         }
1031 
1032         asyncResp->res.jsonValue["@odata.id"] =
1033             "/redfish/v1/AccountService/LDAP/Certificates";
1034         asyncResp->res.jsonValue["@odata.type"] =
1035             "#CertificateCollection.CertificateCollection";
1036         asyncResp->res.jsonValue["Name"] = "LDAP Certificates Collection";
1037         asyncResp->res.jsonValue["Description"] =
1038             "A Collection of LDAP certificate instances";
1039 
1040         getCertificateList(asyncResp, certs::ldapObjectPath,
1041                            "/Members"_json_pointer,
1042                            "/Members@odata.count"_json_pointer);
1043         });
1044 
1045     BMCWEB_ROUTE(app, "/redfish/v1/AccountService/LDAP/Certificates/")
1046         .privileges(redfish::privileges::postCertificateCollection)
1047         .methods(boost::beast::http::verb::post)(
1048             [&app](const crow::Request& req,
1049                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
1050         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
1051         {
1052             return;
1053         }
1054         std::string certFileBody = getCertificateFromReqBody(asyncResp, req);
1055 
1056         if (certFileBody.empty())
1057         {
1058             BMCWEB_LOG_ERROR << "Cannot get certificate from request body.";
1059             messages::unrecognizedRequestBody(asyncResp->res);
1060             return;
1061         }
1062 
1063         std::shared_ptr<CertificateFile> certFile =
1064             std::make_shared<CertificateFile>(certFileBody);
1065 
1066         crow::connections::systemBus->async_method_call(
1067             [asyncResp, certFile](const boost::system::error_code ec,
1068                                   const std::string& objectPath) {
1069             if (ec)
1070             {
1071                 BMCWEB_LOG_ERROR << "DBUS response error: " << ec;
1072                 messages::internalError(asyncResp->res);
1073                 return;
1074             }
1075             long certId = getIDFromURL(objectPath);
1076             if (certId < 0)
1077             {
1078                 BMCWEB_LOG_ERROR << "Invalid objectPath value" << objectPath;
1079                 messages::internalError(asyncResp->res);
1080                 return;
1081             }
1082             std::string certURL =
1083                 "/redfish/v1/AccountService/LDAP/Certificates/" +
1084                 std::to_string(certId);
1085             getCertificateProperties(asyncResp, objectPath,
1086                                      certs::ldapServiceName, certId, certURL,
1087                                      "LDAP Certificate");
1088             BMCWEB_LOG_DEBUG << "LDAP certificate install file="
1089                              << certFile->getCertFilePath();
1090             },
1091             certs::ldapServiceName, certs::ldapObjectPath,
1092             certs::certInstallIntf, "Install", certFile->getCertFilePath());
1093         });
1094 } // requestRoutesLDAPCertificateCollection
1095 
1096 /**
1097  * Certificate resource describes a certificate used to prove the identity
1098  * of a component, account or service.
1099  */
1100 inline void requestRoutesLDAPCertificate(App& app)
1101 {
1102     BMCWEB_ROUTE(app, "/redfish/v1/AccountService/LDAP/Certificates/<str>/")
1103         .privileges(redfish::privileges::getCertificate)
1104         .methods(boost::beast::http::verb::get)(
1105             [&app](const crow::Request& req,
1106                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1107                    const std::string&) {
1108         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
1109         {
1110             return;
1111         }
1112         long id = getIDFromURL(req.url);
1113         if (id < 0)
1114         {
1115             BMCWEB_LOG_ERROR << "Invalid url value" << req.url;
1116             messages::internalError(asyncResp->res);
1117             return;
1118         }
1119         BMCWEB_LOG_DEBUG << "LDAP Certificate ID=" << std::to_string(id);
1120         std::string certURL = "/redfish/v1/AccountService/LDAP/Certificates/" +
1121                               std::to_string(id);
1122         std::string objectPath = certs::ldapObjectPath;
1123         objectPath += "/";
1124         objectPath += std::to_string(id);
1125         getCertificateProperties(asyncResp, objectPath, certs::ldapServiceName,
1126                                  id, certURL, "LDAP Certificate");
1127         });
1128 } // requestRoutesLDAPCertificate
1129 /**
1130  * Collection of TrustStoreCertificate certificates
1131  */
1132 inline void requestRoutesTrustStoreCertificateCollection(App& app)
1133 {
1134     BMCWEB_ROUTE(app, "/redfish/v1/Managers/bmc/Truststore/Certificates/")
1135         .privileges(redfish::privileges::getCertificate)
1136         .methods(boost::beast::http::verb::get)(
1137             [&app](const crow::Request& req,
1138                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
1139         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
1140         {
1141             return;
1142         }
1143 
1144         asyncResp->res.jsonValue["@odata.id"] =
1145             "/redfish/v1/Managers/bmc/Truststore/Certificates/";
1146         asyncResp->res.jsonValue["@odata.type"] =
1147             "#CertificateCollection.CertificateCollection";
1148         asyncResp->res.jsonValue["Name"] = "TrustStore Certificates Collection";
1149         asyncResp->res.jsonValue["Description"] =
1150             "A Collection of TrustStore certificate instances";
1151 
1152         getCertificateList(asyncResp, certs::authorityObjectPath,
1153                            "/Members"_json_pointer,
1154                            "/Members@odata.count"_json_pointer);
1155         });
1156 
1157     BMCWEB_ROUTE(app, "/redfish/v1/Managers/bmc/Truststore/Certificates/")
1158         .privileges(redfish::privileges::postCertificateCollection)
1159         .methods(boost::beast::http::verb::post)(
1160             [&app](const crow::Request& req,
1161                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
1162         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
1163         {
1164             return;
1165         }
1166         std::string certFileBody = getCertificateFromReqBody(asyncResp, req);
1167 
1168         if (certFileBody.empty())
1169         {
1170             BMCWEB_LOG_ERROR << "Cannot get certificate from request body.";
1171             messages::unrecognizedRequestBody(asyncResp->res);
1172             return;
1173         }
1174 
1175         std::shared_ptr<CertificateFile> certFile =
1176             std::make_shared<CertificateFile>(certFileBody);
1177         crow::connections::systemBus->async_method_call(
1178             [asyncResp, certFile](const boost::system::error_code ec,
1179                                   const std::string& objectPath) {
1180             if (ec)
1181             {
1182                 BMCWEB_LOG_ERROR << "DBUS response error: " << ec;
1183                 messages::internalError(asyncResp->res);
1184                 return;
1185             }
1186             long certId = getIDFromURL(objectPath);
1187             if (certId < 0)
1188             {
1189                 BMCWEB_LOG_ERROR << "Invalid objectPath value" << objectPath;
1190                 messages::internalError(asyncResp->res);
1191                 return;
1192             }
1193             std::string certURL =
1194                 "/redfish/v1/Managers/bmc/Truststore/Certificates/" +
1195                 std::to_string(certId);
1196 
1197             getCertificateProperties(asyncResp, objectPath,
1198                                      certs::authorityServiceName, certId,
1199                                      certURL, "TrustStore Certificate");
1200             BMCWEB_LOG_DEBUG << "TrustStore certificate install file="
1201                              << certFile->getCertFilePath();
1202             },
1203             certs::authorityServiceName, certs::authorityObjectPath,
1204             certs::certInstallIntf, "Install", certFile->getCertFilePath());
1205         });
1206 } // requestRoutesTrustStoreCertificateCollection
1207 
1208 /**
1209  * Certificate resource describes a certificate used to prove the identity
1210  * of a component, account or service.
1211  */
1212 inline void requestRoutesTrustStoreCertificate(App& app)
1213 {
1214     BMCWEB_ROUTE(app, "/redfish/v1/Managers/bmc/Truststore/Certificates/<str>/")
1215         .privileges(redfish::privileges::getCertificate)
1216         .methods(boost::beast::http::verb::get)(
1217             [&app](const crow::Request& req,
1218                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1219                    const std::string&) {
1220         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
1221         {
1222             return;
1223         }
1224         long id = getIDFromURL(req.url);
1225         if (id < 0)
1226         {
1227             BMCWEB_LOG_ERROR << "Invalid url value" << req.url;
1228             messages::internalError(asyncResp->res);
1229             return;
1230         }
1231         BMCWEB_LOG_DEBUG << "TrustStoreCertificate::doGet ID="
1232                          << std::to_string(id);
1233         std::string certURL =
1234             "/redfish/v1/Managers/bmc/Truststore/Certificates/" +
1235             std::to_string(id);
1236         std::string objectPath = certs::authorityObjectPath;
1237         objectPath += "/";
1238         objectPath += std::to_string(id);
1239         getCertificateProperties(asyncResp, objectPath,
1240                                  certs::authorityServiceName, id, certURL,
1241                                  "TrustStore Certificate");
1242         });
1243 
1244     BMCWEB_ROUTE(app, "/redfish/v1/Managers/bmc/Truststore/Certificates/<str>/")
1245         .privileges(redfish::privileges::deleteCertificate)
1246         .methods(boost::beast::http::verb::delete_)(
1247             [&app](const crow::Request& req,
1248                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1249                    const std::string& param) {
1250         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
1251         {
1252             return;
1253         }
1254         if (param.empty())
1255         {
1256             messages::internalError(asyncResp->res);
1257             return;
1258         }
1259 
1260         long id = getIDFromURL(req.url);
1261         if (id < 0)
1262         {
1263             BMCWEB_LOG_ERROR << "Invalid url value: " << req.url;
1264             messages::resourceNotFound(asyncResp->res, "TrustStore Certificate",
1265                                        std::string(req.url));
1266             return;
1267         }
1268         BMCWEB_LOG_DEBUG << "TrustStoreCertificate::doDelete ID="
1269                          << std::to_string(id);
1270         std::string certPath = certs::authorityObjectPath;
1271         certPath += "/";
1272         certPath += std::to_string(id);
1273 
1274         crow::connections::systemBus->async_method_call(
1275             [asyncResp, id](const boost::system::error_code ec) {
1276             if (ec)
1277             {
1278                 messages::resourceNotFound(asyncResp->res,
1279                                            "TrustStore Certificate",
1280                                            std::to_string(id));
1281                 return;
1282             }
1283             BMCWEB_LOG_INFO << "Certificate deleted";
1284             asyncResp->res.result(boost::beast::http::status::no_content);
1285             },
1286             certs::authorityServiceName, certPath, certs::objDeleteIntf,
1287             "Delete");
1288         });
1289 } // requestRoutesTrustStoreCertificate
1290 } // namespace redfish
1291