blob: 34102b7b7e180f120f50862ab15fdbc1c75c1686 [file] [log] [blame]
#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