blob: 81d792c6fe627e763152c9a93cceb0f4879511bf [file] [log] [blame] [edit]
#include "tlbmc/credentials/credential_manager.h"
#include <filesystem> // NOLINT
#include <memory>
#include <string>
#include <string_view>
#include <thread> // NOLINT
#include <utility>
#include "absl/log/log.h"
#include "absl/memory/memory.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "g3/macros.h"
#include "tlbmc/redfish/routes/action_managers/file_manager.h"
#include "tlbmc/utils/shell_command_executor.h"
#include "zatar/cert_error_handling.h"
#include "zatar/generate_cert.h"
#include "openssl/asn1.h"
#include "zatar/g3_misc.h"
#include "openssl/bio.h"
#include "openssl/bn.h"
#include "openssl/buffer.h"
#include "openssl/evp.h"
#include "openssl/pem.h"
#include "openssl/rsa.h"
#include "openssl/stack.h"
#include "openssl/x509.h"
namespace milotic_tlbmc {
using ::milotic::authn::GeneratePrivateKey;
using ::milotic::authn::GetSslErrorOnQueue;
using ::milotic::authn::ReturnErrorIfNotSuccess;
using ::milotic::authn::ReturnErrorIfNull;
namespace {
absl::Status AddNameEntry(X509_NAME* name, std::string_view entry,
std::string_view value) {
ECCLESIA_RETURN_IF_ERROR(ReturnErrorIfNotSuccess(
X509_NAME_add_entry_by_txt(
name, std::string(entry).c_str(), MBSTRING_ASC,
reinterpret_cast<const unsigned char*>(value.data()), value.size(),
/*loc=*/-1,
/*set=*/0),
absl::StrCat("Failed to add name entry: ", value, "",
GetSslErrorOnQueue())));
return absl::OkStatus();
}
} // namespace
absl::StatusOr<std::unique_ptr<CredentialManager>> CredentialManager::Create(
std::string_view private_key_path, std::string_view cert_path,
std::string_view owner_verification_cert_rwfs_path,
std::string_view owner_verification_cert_ramfs_path) {
// Check that private key are non empty strings and are files in the same
// folder
if (private_key_path.empty()) {
return absl::InvalidArgumentError("Private key path is empty");
}
if (cert_path.empty()) {
return absl::InvalidArgumentError("Cert path is empty");
}
// Check that the private key and cert are in the same folder
std::filesystem::path private_key_path_fs(private_key_path);
std::filesystem::path cert_path_fs(cert_path);
if (private_key_path_fs.parent_path() != cert_path_fs.parent_path()) {
return absl::InvalidArgumentError(
"Private key and cert are not in the same folder");
}
if (owner_verification_cert_rwfs_path.empty()) {
return absl::InvalidArgumentError(
"Owner verification cert rwfs path is empty");
}
if (owner_verification_cert_ramfs_path.empty()) {
return absl::InvalidArgumentError(
"Owner verification cert ramfs path is empty");
}
return absl::WrapUnique(new CredentialManager(
private_key_path, cert_path, owner_verification_cert_rwfs_path,
owner_verification_cert_ramfs_path));
}
CredentialManager::CredentialManager(
std::string_view private_key_path, std::string_view cert_path,
std::string_view owner_verification_cert_rwfs_path,
std::string_view owner_verification_cert_ramfs_path)
: private_key_path_(private_key_path),
cert_path_(cert_path),
owner_verification_cert_rwfs_path_(owner_verification_cert_rwfs_path),
owner_verification_cert_ramfs_path_(owner_verification_cert_ramfs_path),
command_executor_(std::make_unique<ShellExecutor>()) {
// If the write path already exists, then we should read from there.
absl::StatusOr<std::string> previous_owner_verification_cert =
FileManager::ReadFile(owner_verification_cert_ramfs_path_);
if (previous_owner_verification_cert.ok()) {
LOG(WARNING) << "Owner verification certificate already exists at: "
<< owner_verification_cert_ramfs_path_
<< ". Using the existing owner verification certificate.";
owner_verification_cert_ = std::move(*previous_owner_verification_cert);
return;
}
// Try to read the owner verification certificate from the read path.
absl::StatusOr<std::string> owner_verification_cert =
FileManager::ReadFile(owner_verification_cert_rwfs_path_);
if (owner_verification_cert.ok()) {
owner_verification_cert_ = *owner_verification_cert;
} else {
LOG(ERROR) << "Failed to read owner verification certificate: "
<< owner_verification_cert.status()
<< ". All owner verification certificate validation will fail.";
owner_verification_cert_ = "";
}
absl::Status status = FileManager::WriteToFile(
owner_verification_cert_, owner_verification_cert_ramfs_path_);
if (!status.ok()) {
LOG(ERROR) << "Failed to write owner verification certificate: " << status;
}
}
absl::StatusOr<std::string> CredentialManager::GenerateCsr(
const CsrParams& csr_params) {
// 1. Generate RSA key.
ECCLESIA_ASSIGN_OR_RETURN(bssl::UniquePtr<EVP_PKEY> pkey,
GeneratePrivateKey());
ECCLESIA_RETURN_IF_ERROR(ReturnErrorIfNull(
pkey.get(),
absl::StrCat("Failed to generate private key: ", GetSslErrorOnQueue())));
private_key_ = std::move(pkey);
// 3. Create X509_REQ.
bssl::UniquePtr<X509_REQ> x509(X509_REQ_new());
ECCLESIA_RETURN_IF_ERROR(ReturnErrorIfNull(
x509.get(), absl::StrCat("X509_REQ_new failed: ", GetSslErrorOnQueue())));
// 4. Set subject name.
X509_NAME* subject = X509_REQ_get_subject_name(x509.get());
ECCLESIA_RETURN_IF_ERROR(ReturnErrorIfNull(
subject, absl::StrCat("Failed to get subject name from X509_REQ: ",
GetSslErrorOnQueue())));
ECCLESIA_RETURN_IF_ERROR(AddNameEntry(subject, "C", csr_params.country));
ECCLESIA_RETURN_IF_ERROR(AddNameEntry(subject, "ST", csr_params.state));
ECCLESIA_RETURN_IF_ERROR(AddNameEntry(subject, "L", csr_params.city));
ECCLESIA_RETURN_IF_ERROR(AddNameEntry(subject, "O", csr_params.organization));
ECCLESIA_RETURN_IF_ERROR(
AddNameEntry(subject, "OU", csr_params.organizational_unit));
ECCLESIA_RETURN_IF_ERROR(AddNameEntry(subject, "CN", csr_params.common_name));
// 5. Add Subject Alternative Names
if (!csr_params.alternative_names.empty()) {
std::string san_extension;
for (int i = 0; i < csr_params.alternative_names.size(); ++i) {
std::string_view san = csr_params.alternative_names[i];
// Each SAN needs a DNS: appended to it
absl::StrAppend(&san_extension, "DNS:", san);
// Add a comma between each SAN except the last one
if (i < csr_params.alternative_names.size() - 1) {
absl::StrAppend(&san_extension, ",");
}
}
bssl::UniquePtr<STACK_OF(X509_EXTENSION)> extensions(
sk_X509_EXTENSION_new_null());
bssl::UniquePtr<X509_EXTENSION> san_ext(X509V3_EXT_conf_nid(
nullptr, nullptr, NID_subject_alt_name, san_extension.c_str()));
ECCLESIA_RETURN_IF_ERROR(ReturnErrorIfNull(
san_ext.get(), absl::StrCat("Failed to create SAN extension: ",
GetSslErrorOnQueue())));
ECCLESIA_RETURN_IF_ERROR(ReturnErrorIfNotSuccess(
sk_X509_EXTENSION_push(extensions.get(), san_ext.release()),
absl::StrCat("Failed to add SAN extension to CSR: ",
GetSslErrorOnQueue())));
ECCLESIA_RETURN_IF_ERROR(ReturnErrorIfNotSuccess(
X509_REQ_add_extensions(x509.get(), extensions.get()),
absl::StrCat("Failed to add extensions to CSR: ",
GetSslErrorOnQueue())));
}
// 6. Set public key.
ECCLESIA_RETURN_IF_ERROR(ReturnErrorIfNotSuccess(
X509_REQ_set_pubkey(x509.get(), private_key_.get()),
absl::StrCat("Failed to set public key in CSR: ", GetSslErrorOnQueue())));
// 7. Sign CSR.
ECCLESIA_RETURN_IF_ERROR(ReturnErrorIfNotSuccess(
X509_REQ_sign(x509.get(), private_key_.get(), EVP_sha256()),
absl::StrCat("Failed to sign CSR: ", GetSslErrorOnQueue())));
// 8. Convert CSR to PEM format.
bssl::UniquePtr<BIO> bio_csr(BIO_new(BIO_s_mem()));
ECCLESIA_RETURN_IF_ERROR(ReturnErrorIfNotSuccess(
PEM_write_bio_X509_REQ(bio_csr.get(), x509.get()),
absl::StrCat("Failed to write CSR to BIO: ", GetSslErrorOnQueue())));
char* csr = nullptr;
auto csr_len = BIO_get_mem_data(bio_csr.get(), &csr);
return std::string(csr, csr_len);
}
absl::Status CredentialManager::InstallServerCert(
std::string_view certificate) {
// If GenerateCSR has not been called, we cannot install the cert.
if (private_key_ == nullptr) {
return absl::InternalError(
"GenerateCSR must be called before InstallServerCert");
}
// 1. Load the certificate from the PEM string
bssl::UniquePtr<BIO> bio_cert(BIO_new_mem_buf(
certificate.data(), static_cast<uint32_t>(certificate.length())));
bssl::UniquePtr<X509> cert(
PEM_read_bio_X509(bio_cert.get(), nullptr, nullptr, nullptr));
ECCLESIA_RETURN_IF_ERROR(ReturnErrorIfNull(
cert.get(),
absl::StrCat("Failed to parse certificate PEM: ", GetSslErrorOnQueue())));
ECCLESIA_RETURN_IF_ERROR(ReturnErrorIfNotSuccess(
X509_check_private_key(cert.get(), private_key_.get()),
"Certificate does not match the private key"));
bssl::UniquePtr<BIO> bio_privkey(BIO_new(BIO_s_mem()));
ECCLESIA_RETURN_IF_ERROR(ReturnErrorIfNotSuccess(
PEM_write_bio_PrivateKey(bio_privkey.get(), private_key_.get(), nullptr,
nullptr, 0, nullptr, nullptr),
absl::StrCat("PEM_write_bio_PrivateKey failed: ", GetSslErrorOnQueue())));
char* private_key = nullptr;
auto private_key_len = BIO_get_mem_data(bio_privkey.get(), &private_key);
std::string private_key_pem(private_key, private_key_len);
// Install the cert and key on the machine.
// We must do directory name change here as the key and cert have to both be
// changed atomically
std::string credential_dir = std::filesystem::path(cert_path_).parent_path();
std::string cert_filename = std::filesystem::path(cert_path_).filename();
std::string private_key_filename =
std::filesystem::path(private_key_path_).filename();
// Write to a temp dir
std::string temp_credential_dir = absl::StrCat(credential_dir, "-tmp");
ECCLESIA_RETURN_IF_ERROR(FileManager::WriteToFile(
certificate, absl::StrCat(temp_credential_dir, "/", cert_filename)));
ECCLESIA_RETURN_IF_ERROR(FileManager::WriteToFile(
private_key_pem,
absl::StrCat(temp_credential_dir, "/", private_key_filename)));
// Atomically change both key and cert
ECCLESIA_RETURN_IF_ERROR(
FileManager::RenamePath(temp_credential_dir, credential_dir, true));
std::thread([this]() {
absl::SleepFor(absl::Seconds(kServerCertRestartDelaySeconds));
absl::StatusOr<std::string> status =
command_executor_->Execute(std::string(kRestartBmcWebCommand));
if (!status.ok()) {
LOG(ERROR) << "Failed to restart bmcweb: " << status.status();
}
if (restart_bmcweb_callback_ != nullptr) {
restart_bmcweb_callback_(status.status());
}
}).detach();
return absl::OkStatus();
}
} // namespace milotic_tlbmc