| #include "tlbmc/redfish/routes/action_managers/trust_bundle_manager.h" |
| |
| #include <cstdlib> |
| #include <filesystem> // NOLINT |
| #include <fstream> |
| #include <iterator> |
| #include <string> |
| #include <system_error> // NOLINT |
| #include <thread> // NOLINT |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/cleanup/cleanup.h" |
| #include "absl/functional/any_invocable.h" |
| #include "absl/log/log.h" |
| #include "absl/status/status.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/str_format.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/synchronization/mutex.h" |
| #include "absl/time/clock.h" |
| #include "absl/time/time.h" |
| #include "g3/macros.h" |
| |
| namespace milotic_tlbmc::certificate_service { |
| namespace { |
| |
| constexpr absl::string_view kTempTrustBundlePath = "/tmp/trust_bundle_XXXXXX"; |
| constexpr absl::string_view kTempSignaturePath = "/tmp/signature_XXXXXX"; |
| constexpr absl::string_view kTempOutputFile = "/tmp/openssl_output_XXXXXX"; |
| constexpr absl::string_view kBloomCaCertPath = |
| "/var/google/ca-store/bloom-ca.crt"; |
| |
| // The command to verify the trust bundle signature and verify the signer |
| // certificate with the CA file to make sure the signer is trusted. |
| constexpr absl::string_view kTrustBundleVerifyWithCaCommand = |
| "openssl smime -verify -binary -inform PEM -in '%s' -content '%s' " |
| "-CApath '%s' -purpose any -out /dev/null 2> '%s'"; |
| |
| // The command to verify the trust bundle signature without the CA file. Just |
| // blindly trust the signer certificate bundle in the PKCS#7 signature. |
| constexpr absl::string_view kTrustBundleVerifyNoCaCommand = |
| "openssl smime -verify -binary -inform PEM -in '%s' -content '%s' " |
| "-noverify -purpose any -out /dev/null 2> '%s'"; |
| |
| // The command to install the trust bundle and restart bmcweb. The command will |
| // be executed in a subprocess. And the subprocess will get SIGTERM as bmcweb |
| // service kill mode is group. Here as we use --no-block, it is fine that |
| // the restart command get terminated, it is still better to trap the SIGTERM. |
| constexpr absl::string_view kInstallAndRestartBmcWebCommand = |
| "trap '' TERM; mv %s %s && /usr/bin/systemctl restart --no-block bmcweb"; |
| |
| // The path to the trust bundle file. |
| constexpr absl::string_view kTrustBundlePath = |
| "/var/google/trust_bundle/trust_bundle.pem"; |
| |
| } // namespace |
| |
| void TrustBundleManager::SetCaller( |
| absl::AnyInvocable<int(const char*) const>&& caller) { |
| system_caller_ = std::move(caller); |
| } |
| |
| absl::Status TrustBundleManager::RestartGrpcServer( |
| absl::string_view staged_temp_file_path) { |
| if (staged_temp_file_path.empty()) { |
| return absl::InvalidArgumentError("Staged temp file path is empty."); |
| } |
| |
| int result = |
| system_caller_(absl::StrFormat(kInstallAndRestartBmcWebCommand, |
| staged_temp_file_path, kTrustBundlePath) |
| .c_str()); |
| if (result != 0) { |
| return absl::InternalError( |
| "Issuing restart command failed. The installed trust bundle will " |
| "be applied in next restart."); |
| } |
| return absl::OkStatus(); |
| } |
| |
| absl::StatusOr<StagedTempFile> TrustBundleManager::VerifyAndStageTrustBundle( |
| absl::string_view trust_bundle, absl::string_view signature, |
| absl::string_view ca_file_path) { |
| ECCLESIA_ASSIGN_OR_RETURN( |
| StagedTempFile trust_bundle_file, |
| StagedTempFile::Create(trust_bundle, kTempTrustBundlePath)); |
| ECCLESIA_ASSIGN_OR_RETURN( |
| StagedTempFile signature_file, |
| StagedTempFile::Create(signature, kTempSignaturePath)); |
| ECCLESIA_ASSIGN_OR_RETURN(StagedTempFile output_file, |
| StagedTempFile::Create("", kTempOutputFile)); |
| |
| std::string command; |
| std::error_code ec; |
| if (std::filesystem::exists(ca_file_path, ec)) { |
| command = absl::StrFormat( |
| kTrustBundleVerifyWithCaCommand, signature_file.path(), |
| trust_bundle_file.path(), |
| std::filesystem::path(std::string(ca_file_path)).parent_path().string(), |
| output_file.path()); |
| } else { |
| command = |
| absl::StrFormat(kTrustBundleVerifyNoCaCommand, signature_file.path(), |
| trust_bundle_file.path(), output_file.path()); |
| } |
| |
| int result = std::system(command.c_str()); |
| std::string output; |
| if (std::filesystem::exists(output_file.path(), ec)) { |
| std::ifstream ifs(output_file.path()); |
| if (ifs) { |
| output.assign((std::istreambuf_iterator<char>(ifs)), |
| (std::istreambuf_iterator<char>())); |
| } |
| } |
| LOG(WARNING) << "Trust bundle verify result: " << output; |
| if (result != 0) { |
| return absl::InvalidArgumentError( |
| absl::StrFormat("Failed to verify trust bundle signature. %s", output)); |
| } |
| return trust_bundle_file; |
| } |
| |
| // A simple implementation of File::TempFile that creates a temporary file with |
| // the given content and name template. The file will be deleted when the object |
| // is destroyed. |
| absl::StatusOr<StagedTempFile> StagedTempFile::Create( |
| absl::string_view content, absl::string_view temp_name_template) { |
| std::vector<char> tmpname(temp_name_template.begin(), |
| temp_name_template.end()); |
| tmpname.push_back('\0'); |
| int fd = mkstemp(&tmpname[0]); |
| if (fd == -1) { |
| return absl::InternalError("Failed creating staged temp file"); |
| } |
| |
| absl::Cleanup file_unlink = [&] { unlink(&tmpname[0]); }; |
| absl::Cleanup closefd = [&] { close(fd); }; |
| |
| ssize_t bytes_written = write(fd, content.data(), content.size()); |
| if (bytes_written < 0 || |
| static_cast<size_t>(bytes_written) != content.size()) { |
| return absl::InternalError("Failed writing staged temp file"); |
| } |
| // successfully create the temp file, the staged temp file will be kept so |
| // cancel unlink cleanup. |
| std::move(file_unlink).Cancel(); |
| return StagedTempFile(&tmpname[0]); |
| } |
| |
| StagedTempFile::~StagedTempFile() { |
| if (!path_.empty()) { |
| unlink(path_.c_str()); |
| } |
| } |
| |
| StagedTempFile::StagedTempFile(StagedTempFile&& other) noexcept |
| : path_(std::move(other.path_)) { |
| other.path_.clear(); |
| } |
| |
| StagedTempFile& StagedTempFile::operator=(StagedTempFile&& other) noexcept { |
| if (this != &other) { |
| if (!path_.empty()) { |
| unlink(path_.c_str()); |
| } |
| path_ = std::move(other.path_); |
| other.path_.clear(); |
| } |
| return *this; |
| } |
| |
| StagedTempFile::StagedTempFile(absl::string_view path) : path_(path) {} |
| |
| absl::Status TrustBundleManager::InstallTrustBundle( |
| absl::string_view trust_bundle, absl::string_view signature) { |
| absl::MutexLock lock(mutex_); |
| if (last_install_time_ != absl::InfinitePast() && |
| absl::Now() - last_install_time_ < |
| absl::Seconds(kTrustBundleRetryDelaySeconds)) { |
| std::string error_message = absl::StrFormat( |
| "A trust bundle, installed at %s, is pending to be applied at %s. " |
| "Retry after %s.", |
| absl::FormatTime(last_install_time_), |
| absl::FormatTime(last_install_time_ + |
| absl::Seconds(kTrustBundleRestartDelaySeconds)), |
| absl::FormatTime(last_install_time_ + |
| absl::Seconds(kTrustBundleRetryDelaySeconds))); |
| LOG(ERROR) << error_message; |
| return absl::UnavailableError(error_message); |
| } |
| |
| #ifdef BLOOM_CA_CERT_PATH |
| absl::string_view ca_file_path = BLOOM_CA_CERT_PATH; |
| #else |
| absl::string_view ca_file_path = kBloomCaCertPath; |
| #endif |
| ECCLESIA_ASSIGN_OR_RETURN( |
| StagedTempFile staged_trust_bundle, |
| VerifyAndStageTrustBundle(trust_bundle, signature, ca_file_path)); |
| |
| last_install_time_ = absl::Now(); |
| |
| std::thread([this, staged_temp_file = std::move(staged_trust_bundle)]() { |
| absl::SleepFor(absl::Seconds(kTrustBundleRestartDelaySeconds)); |
| absl::Status status = RestartGrpcServer(staged_temp_file.path()); |
| if (!status.ok()) { |
| LOG(ERROR) << status; |
| } |
| }).detach(); |
| |
| return absl::OkStatus(); |
| } |
| |
| } // namespace milotic_tlbmc::certificate_service |