| #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 |