#include "config.h" #include "certificate.hpp" #include "certs_manager.hpp" #include "x509_utils.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace phosphor::certs { namespace { namespace fs = std::filesystem; using ::phosphor::logging::elog; using InvalidCertificateError = ::sdbusplus::xyz::openbmc_project::Certs::Error::InvalidCertificate; using ::phosphor::logging::xyz::openbmc_project::Certs::InvalidCertificate; using ::sdbusplus::xyz::openbmc_project::Common::Error::InternalFailure; // RAII support for openSSL functions. using BIOMemPtr = std::unique_ptr; using X509StorePtr = std::unique_ptr; using ASN1TimePtr = std::unique_ptr; using EVPPkeyPtr = std::unique_ptr; using BufMemPtr = std::unique_ptr; // Refer to schema 2018.3 // http://redfish.dmtf.org/schemas/v1/Certificate.json#/definitions/KeyUsage for // supported KeyUsage types in redfish // Refer to // https://github.com/openssl/openssl/blob/master/include/openssl/x509v3.h for // key usage bit fields std::map keyUsageToRfStr = { {KU_DIGITAL_SIGNATURE, "DigitalSignature"}, {KU_NON_REPUDIATION, "NonRepudiation"}, {KU_KEY_ENCIPHERMENT, "KeyEncipherment"}, {KU_DATA_ENCIPHERMENT, "DataEncipherment"}, {KU_KEY_AGREEMENT, "KeyAgreement"}, {KU_KEY_CERT_SIGN, "KeyCertSign"}, {KU_CRL_SIGN, "CRLSigning"}, {KU_ENCIPHER_ONLY, "EncipherOnly"}, {KU_DECIPHER_ONLY, "DecipherOnly"}}; // Refer to schema 2018.3 // http://redfish.dmtf.org/schemas/v1/Certificate.json#/definitions/KeyUsage for // supported Extended KeyUsage types in redfish std::map extendedKeyUsageToRfStr = { {NID_server_auth, "ServerAuthentication"}, {NID_client_auth, "ClientAuthentication"}, {NID_email_protect, "EmailProtection"}, {NID_OCSP_sign, "OCSPSigning"}, {NID_ad_timeStamping, "Timestamping"}, {NID_code_sign, "CodeSigning"}}; /** * @brief Dumps the PEM encoded certificate to installFilePath * * @param[in] pem - PEM encoded X509 certificate buffer. * @param[in] certFilePath - Path to the destination file. * * @return void */ void dumpCertificate(const std::string& pem, const std::string& certFilePath) { std::ofstream outputCertFileStream; outputCertFileStream.exceptions( std::ofstream::failbit | std::ofstream::badbit | std::ofstream::eofbit); try { outputCertFileStream.open(certFilePath, std::ios::out); outputCertFileStream << pem << "\n" << std::flush; outputCertFileStream.close(); } catch (const std::exception& e) { lg2::error( "Failed to dump certificate, ERR:{ERR}, SRC_PEM:{SRC_PEM}, DST:{DST}", "ERR", e, "SRC_PEM", pem, "DST", certFilePath); elog(); } } } // namespace void Certificate::copyCertificate(const std::string& certSrcFilePath, const std::string& certFilePath) { try { // Copy the certificate to the installation path // During bootup will be parsing existing file so no need to // copy it. if (certSrcFilePath != certFilePath) { fs::copy(certSrcFilePath, certFilePath, fs::copy_options::overwrite_existing); } } catch (const fs::filesystem_error& e) { lg2::error( "Failed to copy certificate, ERR:{ERR}, SRC:{SRC}, DST:{DST}", "ERR", e.what(), "SRC", certSrcFilePath, "DST", certFilePath); elog(); } } std::string Certificate::generateUniqueFilePath(const std::string& directoryPath) { // Create a template for the temporary file name std::string filePath = directoryPath + "/" + "cert-XXXXXX"; int fd = mkstemp(filePath.data()); if (fd == -1) { lg2::error( "Error occurred while creating random certificate file path, DIR:{DIR}", "DIR", directoryPath); elog(); throw std::runtime_error("Failed to create unique file path"); } // Close the file descriptor and file, just need the unique path close(fd); std::remove(filePath.data()); return filePath; } std::string Certificate::generateAuthCertFileX509Path( const std::string& certSrcFilePath, const std::string& certDstDirPath) { const internal::X509Ptr cert = loadCert(certSrcFilePath); unsigned long hash = X509_subject_name_hash(cert.get()); static constexpr auto certHashLength = 9; char hashBuf[certHashLength]; snprintf(hashBuf, certHashLength, "%08lx", hash); const std::string certHash(hashBuf); for (size_t i = 0; i < maxNumAuthorityCertificates; ++i) { const std::string certDstFileX509Path = certDstDirPath + "/" + certHash + "." + std::to_string(i); if (!fs::exists(certDstFileX509Path)) { return certDstFileX509Path; } } lg2::error("Authority certificate x509 file path already used, DIR:{DIR}", "DIR", certDstDirPath); elog(); } std::string Certificate::generateAuthCertFilePath(const std::string& certSrcFilePath) { // If there is a certificate file path (which means certificate replacement // is doing) use it (do not create new one) if (!certFilePath.empty()) { return certFilePath; } // If source certificate file is located in the certificates directory use // it (do not create new one) else if (fs::path(certSrcFilePath).parent_path().string() == certInstallPath) { return certSrcFilePath; } // Otherwise generate new file name/path else { return generateUniqueFilePath(certInstallPath); } } std::string Certificate::generateCertFilePath(const std::string& certSrcFilePath) { if (certType == CertificateType::authority) { return generateAuthCertFilePath(certSrcFilePath); } else { return certInstallPath; } } Certificate::Certificate(sdbusplus::bus_t& bus, const std::string& objPath, CertificateType type, const std::string& installPath, const std::string& uploadPath, Watch* watch, Manager& parent, bool restore) : internal::CertificateInterface( bus, objPath.c_str(), internal::CertificateInterface::action::defer_emit), objectPath(objPath), certType(type), certInstallPath(installPath), certWatch(watch), manager(parent) { auto installHelper = [this](const auto& filePath) { if (!compareKeys(filePath)) { elog(InvalidCertificate::REASON( "Private key does not match the Certificate")); }; }; typeFuncMap[CertificateType::server] = installHelper; typeFuncMap[CertificateType::client] = installHelper; typeFuncMap[CertificateType::authority] = [](const std::string&) {}; auto appendPrivateKey = [this](const std::string& filePath) { checkAndAppendPrivateKey(filePath); }; appendKeyMap[CertificateType::server] = appendPrivateKey; appendKeyMap[CertificateType::client] = appendPrivateKey; appendKeyMap[CertificateType::authority] = [](const std::string&) {}; // Generate certificate file path certFilePath = generateCertFilePath(uploadPath); // install the certificate install(uploadPath, restore); this->emit_object_added(); } Certificate::Certificate(sdbusplus::bus_t& bus, const std::string& objPath, const CertificateType& type, const std::string& installPath, X509_STORE& x509Store, const std::string& pem, Watch* watchPtr, Manager& parent, bool restore) : internal::CertificateInterface( bus, objPath.c_str(), internal::CertificateInterface::action::defer_emit), objectPath(objPath), certType(type), certInstallPath(installPath), certWatch(watchPtr), manager(parent) { // Generate certificate file path certFilePath = generateUniqueFilePath(installPath); // install the certificate install(x509Store, pem, restore); this->emit_object_added(); } Certificate::~Certificate() { if (!fs::remove(certFilePath)) { lg2::info("Certificate file not found! PATH:{PATH}", "PATH", certFilePath); } } void Certificate::replace(const std::string filePath) { manager.replaceCertificate(this, filePath); } void Certificate::install(const std::string& certSrcFilePath, bool restore) { if (restore) { lg2::debug("Certificate install, FILEPATH:{FILEPATH}", "FILEPATH", certSrcFilePath); } else { lg2::info("Certificate install, FILEPATH:{FILEPATH}", "FILEPATH", certSrcFilePath); } // stop watch for user initiated certificate install if (certWatch != nullptr) { certWatch->stopWatch(); } // Verify the certificate file fs::path file(certSrcFilePath); if (!fs::exists(file)) { lg2::error("File is Missing, FILE:{FILE}", "FILE", certSrcFilePath); elog(); } try { if (fs::file_size(certSrcFilePath) == 0) { // file is empty lg2::error("File is empty, FILE:{FILE}", "FILE", certSrcFilePath); elog( InvalidCertificate::REASON("File is empty")); } } catch (const fs::filesystem_error& e) { // Log Error message lg2::error("File is empty, FILE:{FILE}, ERR:{ERR}", "FILE", certSrcFilePath, "ERR", e); elog(); } X509StorePtr x509Store = getX509Store(certSrcFilePath); // Load Certificate file into the X509 structure. internal::X509Ptr cert = loadCert(certSrcFilePath); // Perform validation validateCertificateAgainstStore(*x509Store, *cert); validateCertificateStartDate(*cert); validateCertificateInSSLContext(*cert); // Invoke type specific append private key function. if (auto it = appendKeyMap.find(certType); it == appendKeyMap.end()) { lg2::error("Unsupported Type, TYPE:{TYPE}", "TYPE", certificateTypeToString(certType)); elog(); } else { it->second(certSrcFilePath); } // Invoke type specific compare keys function. if (auto it = typeFuncMap.find(certType); it == typeFuncMap.end()) { lg2::error("Unsupported Type, TYPE:{TYPE}", "TYPE", certificateTypeToString(certType)); elog(); } else { it->second(certSrcFilePath); } copyCertificate(certSrcFilePath, certFilePath); storageUpdate(); // Keep certificate ID certId = generateCertId(*cert); // Parse the certificate file and populate properties populateProperties(*cert); // restart watch if (certWatch != nullptr) { certWatch->startWatch(); } } void Certificate::install(X509_STORE& x509Store, const std::string& pem, bool restore) { if (restore) { lg2::debug("Certificate install, PEM_STR:{PEM_STR}", "PEM_STR", pem); } else { lg2::info("Certificate install, PEM_STR:{PEM_STR} ", "PEM_STR", pem); } if (certType != CertificateType::authority) { lg2::error("Bulk install error: Unsupported Type; only authority " "supports bulk install, TYPE:{TYPE}", "TYPE", certificateTypeToString(certType)); elog(); } // stop watch for user initiated certificate install if (certWatch) { certWatch->stopWatch(); } // Load Certificate file into the X509 structure. internal::X509Ptr cert = parseCert(pem); // Perform validation; no type specific compare keys function validateCertificateAgainstStore(x509Store, *cert); validateCertificateStartDate(*cert); validateCertificateInSSLContext(*cert); // Copy the PEM to the installation path dumpCertificate(pem, certFilePath); storageUpdate(); // Keep certificate ID certId = generateCertId(*cert); // Parse the certificate file and populate properties populateProperties(*cert); // restart watch if (certWatch) { certWatch->startWatch(); } } void Certificate::populateProperties() { internal::X509Ptr cert = loadCert(certInstallPath); populateProperties(*cert); } std::string Certificate::getCertId() const { return certId; } bool Certificate::isSame(const std::string& certPath) { internal::X509Ptr cert = loadCert(certPath); return getCertId() == generateCertId(*cert); } void Certificate::storageUpdate() { if (certType == CertificateType::authority) { // Create symbolic link in the certificate directory std::string certFileX509Path; try { if (!certFilePath.empty() && fs::is_regular_file(fs::path(certFilePath))) { certFileX509Path = generateAuthCertFileX509Path(certFilePath, certInstallPath); fs::create_symlink(fs::path(certFilePath), fs::path(certFileX509Path)); } } catch (const std::exception& e) { lg2::error("Failed to create symlink for certificate, ERR:{ERR}," "FILE:{FILE}, SYMLINK:{SYMLINK}", "ERR", e, "FILE", certFilePath, "SYMLINK", certFileX509Path); elog(); } } } void Certificate::populateProperties(X509& cert) { // Update properties if no error thrown BIOMemPtr certBio(BIO_new(BIO_s_mem()), BIO_free); PEM_write_bio_X509(certBio.get(), &cert); BufMemPtr certBuf(BUF_MEM_new(), BUF_MEM_free); BUF_MEM* buf = certBuf.get(); BIO_get_mem_ptr(certBio.get(), &buf); std::string certStr(buf->data, buf->length); certificateString(certStr); static const int maxKeySize = 4096; char subBuffer[maxKeySize] = {0}; BIOMemPtr subBio(BIO_new(BIO_s_mem()), BIO_free); // This pointer cannot be freed independently. X509_NAME* sub = X509_get_subject_name(&cert); X509_NAME_print_ex(subBio.get(), sub, 0, XN_FLAG_SEP_COMMA_PLUS); BIO_read(subBio.get(), subBuffer, maxKeySize); subject(subBuffer); char issuerBuffer[maxKeySize] = {0}; BIOMemPtr issuerBio(BIO_new(BIO_s_mem()), BIO_free); // This pointer cannot be freed independently. X509_NAME* issuerName = X509_get_issuer_name(&cert); X509_NAME_print_ex(issuerBio.get(), issuerName, 0, XN_FLAG_SEP_COMMA_PLUS); BIO_read(issuerBio.get(), issuerBuffer, maxKeySize); issuer(issuerBuffer); std::vector keyUsageList; // Go through each usage in the bit string and convert to // corresponding string value ASN1_BIT_STRING* usage = static_cast( X509_get_ext_d2i(&cert, NID_key_usage, nullptr, nullptr)); if (usage != nullptr) { for (auto i = 0; i < usage->length; ++i) { for (auto& x : keyUsageToRfStr) { if (x.first & usage->data[i]) { keyUsageList.push_back(x.second); break; } } } } EXTENDED_KEY_USAGE* extUsage = static_cast( X509_get_ext_d2i(&cert, NID_ext_key_usage, nullptr, nullptr)); if (extUsage == nullptr) { for (int i = 0; i < sk_ASN1_OBJECT_num(extUsage); i++) { keyUsageList.push_back(extendedKeyUsageToRfStr[OBJ_obj2nid( sk_ASN1_OBJECT_value(extUsage, i))]); } } keyUsage(keyUsageList); int days = 0; int secs = 0; ASN1TimePtr epoch(ASN1_TIME_new(), ASN1_STRING_free); // Set time to 00:00am GMT, Jan 1 1970; format: YYYYMMDDHHMMSSZ ASN1_TIME_set_string(epoch.get(), "19700101000000Z"); constexpr uint64_t dayToSeconds = 86400; // 24 * 60 * 60 ASN1_TIME* notAfter = X509_get_notAfter(&cert); ASN1_TIME_diff(&days, &secs, epoch.get(), notAfter); validNotAfter((days * dayToSeconds) + secs); ASN1_TIME* notBefore = X509_get_notBefore(&cert); ASN1_TIME_diff(&days, &secs, epoch.get(), notBefore); validNotBefore((days * dayToSeconds) + secs); } void Certificate::checkAndAppendPrivateKey(const std::string& filePath) { BIOMemPtr keyBio(BIO_new(BIO_s_file()), ::BIO_free); if (!keyBio) { lg2::error("Error occurred during BIO_s_file call, FILE:{FILE}", "FILE", filePath); elog(); } BIO_read_filename(keyBio.get(), filePath.c_str()); EVPPkeyPtr priKey( PEM_read_bio_PrivateKey(keyBio.get(), nullptr, nullptr, nullptr), ::EVP_PKEY_free); if (!priKey) { lg2::info("Private key not present in file, FILE:{FILE}", "FILE", filePath); fs::path privateKeyFile = fs::path(certInstallPath).parent_path(); privateKeyFile = privateKeyFile / defaultPrivateKeyFileName; if (!fs::exists(privateKeyFile)) { lg2::error("Private key file is not found, FILE:{FILE}", "FILE", privateKeyFile); elog(); } std::ifstream privKeyFileStream; std::ofstream certFileStream; privKeyFileStream.exceptions( std::ifstream::failbit | std::ifstream::badbit | std::ifstream::eofbit); certFileStream.exceptions( std::ofstream::failbit | std::ofstream::badbit | std::ofstream::eofbit); try { privKeyFileStream.open(privateKeyFile); certFileStream.open(filePath, std::ios::app); certFileStream << std::endl; // insert line break certFileStream << privKeyFileStream.rdbuf() << std::flush; privKeyFileStream.close(); certFileStream.close(); } catch (const std::exception& e) { lg2::error( "Failed to append private key, ERR:{ERR}, SRC:{SRC}, DST:{DST}", "ERR", e, "SRC", privateKeyFile, "DST", filePath); elog(); } } } bool Certificate::compareKeys(const std::string& filePath) { lg2::info("Certificate compareKeys, FILEPATH:{FILEPATH}", "FILEPATH", filePath); internal::X509Ptr cert(X509_new(), ::X509_free); if (!cert) { lg2::error( "Error occurred during X509_new call, FILE:{FILE}, ERRCODE:{ERRCODE}", "FILE", filePath, "ERRCODE", ERR_get_error()); elog(); } BIOMemPtr bioCert(BIO_new_file(filePath.c_str(), "rb"), ::BIO_free); if (!bioCert) { lg2::error("Error occurred during BIO_new_file call, FILE:{FILE}", "FILE", filePath); elog(); } X509* x509 = cert.get(); PEM_read_bio_X509(bioCert.get(), &x509, nullptr, nullptr); EVPPkeyPtr pubKey(X509_get_pubkey(cert.get()), ::EVP_PKEY_free); if (!pubKey) { lg2::error( "Error occurred during X509_get_pubkey, FILE:{FILE}, ERRCODE:{ERRCODE}", "FILE", filePath, "ERRCODE", ERR_get_error()); elog( InvalidCertificate::REASON("Failed to get public key info")); } BIOMemPtr keyBio(BIO_new(BIO_s_file()), ::BIO_free); if (!keyBio) { lg2::error("Error occurred during BIO_s_file call, FILE:{FILE}", "FILE", filePath); elog(); } BIO_read_filename(keyBio.get(), filePath.c_str()); EVPPkeyPtr priKey( PEM_read_bio_PrivateKey(keyBio.get(), nullptr, nullptr, nullptr), ::EVP_PKEY_free); if (!priKey) { lg2::error( "Error occurred during PEM_read_bio_PrivateKey, FILE:{FILE}, ERRCODE:{ERRCODE}", "FILE", filePath, "ERRCODE", ERR_get_error()); elog( InvalidCertificate::REASON("Failed to get private key info")); } #if (OPENSSL_VERSION_NUMBER < 0x30000000L) int32_t rc = EVP_PKEY_cmp(priKey.get(), pubKey.get()); #else int32_t rc = EVP_PKEY_eq(priKey.get(), pubKey.get()); #endif if (rc != 1) { lg2::error( "Private key is not matching with Certificate, FILE:{FILE}, ERRCODE:{ERRCODE}", "FILE", filePath, "ERRCODE", rc); return false; } return true; } void Certificate::delete_() { manager.deleteCertificate(this); } std::string Certificate::getObjectPath() { return objectPath; } std::string Certificate::getCertFilePath() { return certFilePath; } void Certificate::setCertFilePath(const std::string& path) { certFilePath = path; } void Certificate::setCertInstallPath(const std::string& path) { certInstallPath = path; } } // namespace phosphor::certs