| #include "install/installer.h" | |
| #include <sys/wait.h> | |
| #include <unistd.h> | |
| #include <algorithm> | |
| #include <cmath> | |
| #include <cstddef> | |
| #include <filesystem> // NOLINT | |
| #include <fstream> | |
| #include <ios> | |
| #include <limits> | |
| #include <sstream> | |
| #include <string> | |
| #include <system_error> // NOLINT | |
| #include <vector> | |
| #include "owner_certificate/owner_verification_cert_configuration.pb.h" | |
| #include "absl/cleanup/cleanup.h" | |
| #include "absl/container/flat_hash_map.h" | |
| #include "absl/container/flat_hash_set.h" | |
| #include "absl/log/log.h" | |
| #include "absl/status/status.h" | |
| #include "absl/strings/ascii.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/str_join.h" | |
| #include "absl/strings/str_split.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/strings/substitute.h" | |
| #include "absl/time/clock.h" | |
| #include "absl/time/time.h" | |
| #include <nlohmann/json.hpp> | |
| #include "google/protobuf/text_format.h" | |
| namespace bloom_install { | |
| using ::bloom_install::internal::StatusReporter; | |
| // LINT.IfChange | |
| constexpr char kStatusKeyStatus[] = "status"; | |
| constexpr char kStatusKeyPercentComplete[] = "percent_complete"; | |
| constexpr char kStatusKeyMessage[] = "message"; | |
| constexpr char kStatusKeyStartTime[] = "start_time"; | |
| constexpr char kStatusKeyLastUpdateTime[] = "last_update_time"; | |
| // LINT.ThenChange(//depot/google3/third_party/milotic/external/cc/tlbmc/redfish/routes/task_service.cc) | |
| ProgressLogger::ProgressLogger(const std::string& log_file_path) | |
| : log_file_path_(log_file_path), | |
| log_file_(log_file_path_, std::ios_base::app), | |
| is_healthy_(log_file_.is_open()) {} | |
| bool ProgressLogger::IsHealthy() const { return is_healthy_; } | |
| void ProgressLogger::Log(State state, absl::Duration elapsed_time, | |
| const std::string& message, LogCode code, | |
| int retries) { | |
| if (!is_healthy_) { | |
| return; | |
| } | |
| nlohmann::json log_entry; | |
| log_entry["state"] = StateToString(state); | |
| log_entry["time"] = absl::ToDoubleSeconds(elapsed_time); | |
| log_entry["message"] = message; | |
| log_entry["code"] = LogCodeToString(code); | |
| if (state == State::kFailureRetry) { | |
| log_entry["retries"] = retries; | |
| } | |
| // TODO(nanzhou): Remove dump. It's not very useful as we have separate | |
| // journal logs. I kept them because of the test was checking the files. | |
| const std::string log_str = log_entry.dump(); | |
| log_file_ << log_str << "\n"; | |
| LOG(INFO) << log_str; | |
| log_file_.flush(); | |
| } | |
| namespace internal { | |
| StatusReporter StatusReporter::Create(const std::string& status_path, | |
| int total_packages) { | |
| return StatusReporter(status_path, total_packages); | |
| } | |
| StatusReporter::StatusReporter(const std::string& status_path, | |
| int total_packages) | |
| : status_path_(status_path), total_packages_(std::max(total_packages, 1)) {} | |
| void StatusReporter::Start(const std::string& message) { | |
| status_json_[kStatusKeyStatus] = "New"; | |
| status_json_[kStatusKeyPercentComplete] = 0; | |
| status_json_[kStatusKeyMessage] = message; | |
| const std::string now_str = absl::FormatTime(absl::Now()); | |
| status_json_[kStatusKeyStartTime] = now_str; | |
| status_json_[kStatusKeyLastUpdateTime] = now_str; | |
| WriteStatus(); | |
| } | |
| void StatusReporter::RecordSuccess(const std::string& message) { | |
| if (packages_completed_ < total_packages_) { | |
| packages_completed_++; | |
| } | |
| UpdateProgress(message); | |
| } | |
| void StatusReporter::UpdateProgress(const std::string& message) { | |
| status_json_[kStatusKeyPercentComplete] = | |
| std::round((packages_completed_ * 100.0) / total_packages_); | |
| status_json_[kStatusKeyMessage] = message; | |
| status_json_[kStatusKeyLastUpdateTime] = absl::FormatTime(absl::Now()); | |
| WriteStatus(); | |
| } | |
| void StatusReporter::Complete(bool success, const std::string& message) { | |
| status_json_[kStatusKeyStatus] = success ? "Completed" : "Exception"; | |
| if (success) { | |
| status_json_[kStatusKeyPercentComplete] = 100; | |
| } | |
| status_json_[kStatusKeyMessage] = message; | |
| status_json_[kStatusKeyLastUpdateTime] = absl::FormatTime(absl::Now()); | |
| WriteStatus(); | |
| } | |
| void StatusReporter::WriteStatus() { | |
| const std::string temp_path = status_path_ + ".tmp"; | |
| { | |
| std::ofstream temp_file(temp_path); | |
| if (!temp_file.is_open()) { | |
| return; | |
| } | |
| temp_file << status_json_.dump(4); | |
| } | |
| std::error_code ec; | |
| if (std::filesystem::rename(temp_path, status_path_, ec); ec) { | |
| std::filesystem::remove(temp_path); | |
| } | |
| } | |
| // Helper to get the set of files listed in the manifest. | |
| absl::flat_hash_set<std::string> GetManifestFiles( | |
| const std::string& sha256sums_file_path) { | |
| absl::flat_hash_set<std::string> manifest_files; | |
| std::ifstream sums_file(sha256sums_file_path); | |
| if (!sums_file.is_open()) { | |
| LOG(ERROR) << "Could not open manifest file: " << sha256sums_file_path; | |
| return manifest_files; | |
| } | |
| std::string line; | |
| while (std::getline(sums_file, line)) { | |
| // sha256sum format is: <hash> <mode><file_path> | |
| // mode is ' ' for text and '*' for binary. | |
| size_t first_space = line.find(' '); | |
| if (first_space == std::string::npos || first_space + 2 > line.length()) { | |
| continue; | |
| } | |
| absl::string_view path(line); | |
| // Remove the hash, the space and the 1-character mode flag (* or [space]). | |
| path.remove_prefix(first_space + 2); | |
| manifest_files.insert( | |
| std::filesystem::path(path).lexically_normal().string()); | |
| } | |
| return manifest_files; | |
| } | |
| // Returns true if the directory entry represents a valid package. | |
| // A valid package must be a directory containing both post_install.sh and | |
| // activation_check.sh, and both files must be present in the manifest. | |
| bool IsPackageValid(const std::string& package_path_str, | |
| const absl::flat_hash_set<std::string>& manifest_files) { | |
| std::error_code ec; | |
| std::filesystem::path package_path(package_path_str); | |
| if (!std::filesystem::is_directory(package_path, ec)) { | |
| return false; | |
| } | |
| if (!std::filesystem::exists(package_path / "post_install.sh", ec) || | |
| !std::filesystem::exists(package_path / "activation_check.sh", ec)) { | |
| return false; | |
| } | |
| std::string post_install_path = (std::filesystem::path("firmware_bundle") / | |
| package_path.filename() / "post_install.sh") | |
| .string(); | |
| std::string activation_check_path = | |
| (std::filesystem::path("firmware_bundle") / package_path.filename() / | |
| "activation_check.sh") | |
| .string(); | |
| return manifest_files.contains(post_install_path) && | |
| manifest_files.contains(activation_check_path); | |
| } | |
| std::vector<std::string> GetPostInstallOrder( | |
| const std::string& config_file_path) { | |
| std::vector<std::string> post_install_order; | |
| std::ifstream config_file(config_file_path); | |
| if (!config_file.is_open()) { | |
| LOG(INFO) << "Could not open bundle configuration file: " | |
| << config_file_path; | |
| return post_install_order; | |
| } | |
| nlohmann::json config = nlohmann::json::parse(config_file, nullptr, false); | |
| if (!config.is_object()) { | |
| LOG(ERROR) << "Could not parse bundle configuration file as a JSON " | |
| "object: " | |
| << config_file_path; | |
| return post_install_order; | |
| } | |
| const auto order = config.find(kPostInstallOrderKey); | |
| if (order == config.end()) { | |
| LOG(INFO) << "Bundle configuration file has no " << kPostInstallOrderKey | |
| << " field: " << config_file_path; | |
| return post_install_order; | |
| } | |
| if (!order->is_array()) { | |
| LOG(ERROR) << kPostInstallOrderKey << " is not an array in bundle " | |
| << "configuration file: " << config_file_path; | |
| return post_install_order; | |
| } | |
| for (const nlohmann::json& package_name : *order) { | |
| if (!package_name.is_string()) { | |
| LOG(ERROR) << "Ignoring non-string entry in " << kPostInstallOrderKey | |
| << ": " << package_name.dump(); | |
| continue; | |
| } | |
| post_install_order.push_back(package_name.get<std::string>()); | |
| } | |
| return post_install_order; | |
| } | |
| } // namespace internal | |
| Installer::Installer(const InstallerOptions& options) | |
| : options_(options), | |
| logger_(options.log_path), | |
| status_reporter_(StatusReporter::Create( | |
| options.status_path, static_cast<int>(options.packages.size()))) { | |
| if (options.packages.empty()) { | |
| LOG(WARNING) << "No packages to install. Post install or activation " | |
| "check will succeed with no-op."; | |
| } | |
| } | |
| // Helper function to verify a detached signature using OpenSSL. | |
| bool Installer::VerifyDetachedSignature(const std::string& file_to_verify, | |
| const std::string& signature_file, | |
| const std::string& public_key_file) { | |
| absl::Time check_start_time = absl::Now(); | |
| std::vector<std::string> args = { | |
| "openssl", "dgst", "-sha256", "-verify", | |
| public_key_file, "-signature", signature_file, file_to_verify}; | |
| int result = ExecuteCommand(args); | |
| if (result != 0) { | |
| const std::string error_msg = absl::Substitute( | |
| "Detached signature verification failed for $0 with key $1 (exit code: " | |
| "$2)", | |
| file_to_verify, public_key_file, result); | |
| logger_.Log(State::kCheckingBundleSignature, absl::Now() - check_start_time, | |
| error_msg, LogCode::kBundleSignatureFail); | |
| return false; | |
| } | |
| const std::string success_msg = absl::Substitute( | |
| "Successfully verified detached signature for $0 with key $1", | |
| file_to_verify, public_key_file); | |
| logger_.Log(State::kSuccess, absl::Now() - check_start_time, success_msg, | |
| LogCode::kBundleSignatureSuccess); | |
| return true; | |
| } | |
| bool Installer::CheckBundleSignature( | |
| const std::string& sha256sums_file_path, | |
| const std::string& signature_file_path, | |
| const std::vector<std::string>& public_key_paths) { | |
| absl::Time check_start_time = absl::Now(); | |
| const std::string start_msg = "Starting bundle signature check"; | |
| logger_.Log(State::kCheckingBundleSignature, absl::ZeroDuration(), start_msg, | |
| LogCode::kBundleSignatureCheckStart); | |
| status_reporter_.Start(start_msg); | |
| // First, verify signature over the sums file | |
| bool signature_verified = false; | |
| for (const std::string& public_key_path : public_key_paths) { | |
| if (VerifyDetachedSignature(sha256sums_file_path, signature_file_path, | |
| public_key_path)) { | |
| signature_verified = true; | |
| break; | |
| } | |
| } | |
| if (!signature_verified) { | |
| const std::string error_msg = "Detached signature verification failed."; | |
| logger_.Log(State::kFailureFatal, absl::Now() - check_start_time, error_msg, | |
| LogCode::kBundleSignatureFail); | |
| status_reporter_.Complete(false, error_msg); | |
| return false; | |
| } | |
| // Now verify that file hashes match the sums file | |
| std::filesystem::path sums_file_parent_path = | |
| std::filesystem::path(sha256sums_file_path).parent_path(); | |
| std::vector<std::string> args = {"sha256sum", "-c", sha256sums_file_path}; | |
| int result = ExecuteCommand(args, sums_file_parent_path.string()); | |
| if (result != 0) { | |
| const std::string error_msg = absl::Substitute( | |
| "Bundle file hash verification failed for command 'sha256sum -c $0'. " | |
| "Exit code: $1", | |
| sha256sums_file_path, result); | |
| logger_.Log(State::kFailureFatal, absl::Now() - check_start_time, error_msg, | |
| LogCode::kBundleHashFail); | |
| status_reporter_.Complete(false, error_msg); | |
| return false; | |
| } | |
| const std::string success_msg = "Bundle signature check successful."; | |
| logger_.Log(State::kSuccess, absl::Now() - check_start_time, success_msg, | |
| LogCode::kBundleSignatureSuccess); | |
| return true; | |
| } | |
| absl::StatusOr<owner_certificate::OwnerVerificationCertConfiguration> | |
| Installer::GetOwnerConfiguration(const std::string& config_file_path) { | |
| absl::Time start_time = absl::Now(); | |
| const std::string start_msg = absl::Substitute( | |
| "Starting to retrieve owner configuration from $0", config_file_path); | |
| logger_.Log(State::kGettingOwnerConfiguration, absl::ZeroDuration(), | |
| start_msg, LogCode::kGetOwnerConfigStart); | |
| std::ifstream config_file(config_file_path); | |
| if (!config_file.is_open()) { | |
| std::string msg = absl::Substitute( | |
| "Could not open owner configuration file: $0", config_file_path); | |
| LOG(ERROR) << msg; | |
| logger_.Log(State::kFailureFatal, absl::Now() - start_time, msg, | |
| LogCode::kGetOwnerConfigFail); | |
| return absl::NotFoundError(msg); | |
| } | |
| std::stringstream buffer; | |
| buffer << config_file.rdbuf(); | |
| if (config_file.bad()) { | |
| std::string msg = absl::Substitute( | |
| "Failed to read owner configuration file from $0", config_file_path); | |
| LOG(ERROR) << msg; | |
| logger_.Log(State::kFailureFatal, absl::Now() - start_time, msg, | |
| LogCode::kGetOwnerConfigFail); | |
| return absl::InternalError(msg); | |
| } | |
| std::string config_str = buffer.str(); | |
| if (absl::StripAsciiWhitespace(config_str).empty()) { | |
| std::string msg = absl::Substitute( | |
| "Owner configuration file $0 is empty or contains only whitespace.", | |
| config_file_path); | |
| LOG(ERROR) << msg; | |
| logger_.Log(State::kFailureFatal, absl::Now() - start_time, msg, | |
| LogCode::kGetOwnerConfigFail); | |
| return absl::InternalError(msg); | |
| } | |
| owner_certificate::OwnerVerificationCertConfiguration config; | |
| // TODO(b/489183188): Remove this temporary default once all test machines | |
| // have the field deployed. | |
| config.set_bundle_dev_verification_enabled(true); | |
| if (!::google::protobuf::TextFormat::MergeFromString(config_str, &config)) { | |
| std::string msg = | |
| absl::Substitute("Failed to parse $0 as a textproto", config_file_path); | |
| LOG(ERROR) << msg; | |
| logger_.Log(State::kFailureFatal, absl::Now() - start_time, msg, | |
| LogCode::kGetOwnerConfigFail); | |
| return absl::InvalidArgumentError(msg); | |
| } | |
| const std::string success_msg = "Successfully retrieved owner configuration"; | |
| logger_.Log(State::kSuccess, absl::Now() - start_time, success_msg, | |
| LogCode::kGetOwnerConfigSuccess); | |
| return config; | |
| } | |
| bool Installer::PostInstall() { | |
| absl::Time total_start_time = absl::Now(); | |
| if (options_.packages.empty()) { | |
| const std::string complete_msg = | |
| "No packages to install. Process complete."; | |
| logger_.Log(State::kSuccess, absl::Now() - total_start_time, complete_msg, | |
| LogCode::kInstallComplete); | |
| return true; | |
| } | |
| const std::string start_msg = "Starting installation process."; | |
| logger_.Log(State::kStarting, absl::ZeroDuration(), start_msg, | |
| LogCode::kInitStart); | |
| status_reporter_.Start(start_msg); | |
| int packages_completed_for_logging = 0; | |
| for (const auto& package : options_.packages) { | |
| if (!InstallPackage(package)) { | |
| const std::string failure_msg = | |
| absl::Substitute("Installation failed on package: $0", package.name); | |
| absl::Duration elapsed = absl::Now() - total_start_time; | |
| logger_.Log( | |
| State::kFailureFatal, elapsed, | |
| absl::Substitute("Installation halted due to failure in package: $0", | |
| package.name), | |
| LogCode::kInstallHalt); | |
| status_reporter_.Complete(false, failure_msg); | |
| return false; | |
| } | |
| // Persist the activation scripts togather with other scripts to the | |
| // activation check directory. | |
| std::filesystem::path activation_package_dir = | |
| options_.activation_check_dir; | |
| activation_package_dir /= package.name; | |
| std::error_code ec; | |
| if (std::filesystem::create_directories(activation_package_dir, ec); ec) { | |
| const std::string failure_msg = absl::Substitute( | |
| "Could not create activation check script directory: $0 with error: " | |
| "$1", | |
| activation_package_dir.c_str(), ec.message()); | |
| logger_.Log(State::kFailureFatal, absl::Now() - total_start_time, | |
| failure_msg, LogCode::kInstallHalt); | |
| status_reporter_.Complete(false, failure_msg); | |
| return false; | |
| } | |
| const std::filesystem::path package_dir = | |
| std::filesystem::path(package.activation_script_path).parent_path(); | |
| ec.clear(); | |
| for (auto it = std::filesystem::directory_iterator(package_dir, ec); | |
| it != std::filesystem::directory_iterator(); it.increment(ec)) { | |
| if (ec) { | |
| break; | |
| } | |
| std::filesystem::directory_entry file_entry = *it; | |
| if (!file_entry.is_regular_file()) { | |
| continue; | |
| } | |
| if (file_entry.path().extension() == ".sh" || | |
| file_entry.path().filename() == "VERSION") { | |
| std::filesystem::path dest_path = activation_package_dir; | |
| dest_path /= file_entry.path().filename(); | |
| std::error_code copy_ec; | |
| if (std::filesystem::copy( | |
| file_entry.path(), dest_path, | |
| std::filesystem::copy_options::overwrite_existing, copy_ec); | |
| copy_ec) { | |
| const std::string failure_msg = absl::Substitute( | |
| "Could not copy file: $0 to: $1 with error: $2", | |
| file_entry.path().c_str(), dest_path.c_str(), copy_ec.message()); | |
| logger_.Log(State::kFailureFatal, absl::Now() - total_start_time, | |
| failure_msg, LogCode::kInstallHalt); | |
| status_reporter_.Complete(false, failure_msg); | |
| return false; | |
| } | |
| } | |
| } | |
| packages_completed_for_logging++; | |
| const std::string progress_msg = absl::Substitute( | |
| "Successfully completed package $0 ($1 of $2).", package.name, | |
| packages_completed_for_logging, options_.packages.size()); | |
| status_reporter_.RecordSuccess(progress_msg); | |
| } | |
| const std::string success_msg = "All packages installed successfully."; | |
| absl::Duration elapsed = absl::Now() - total_start_time; | |
| logger_.Log(State::kSuccess, elapsed, success_msg, LogCode::kInstallComplete); | |
| status_reporter_.Complete(true, success_msg); | |
| return true; | |
| } | |
| bool Installer::InstallPackage(const Package& package) { | |
| absl::Time check_start_time = absl::Now(); | |
| logger_.Log( | |
| State::kCheckingActivation, absl::ZeroDuration(), | |
| absl::Substitute("Checking activation for package: $0", package.name), | |
| LogCode::kPkgActivationCheckStart); | |
| int activation_return_code = ExecuteCommand(package.activation_script_path); | |
| absl::Duration check_elapsed = absl::Now() - check_start_time; | |
| if (activation_return_code == 0) { | |
| logger_.Log( | |
| State::kSkipped, check_elapsed, | |
| absl::Substitute("Package already active, skipping installation: $0", | |
| package.name), | |
| LogCode::kPkgActivationSkip); | |
| return true; | |
| } | |
| logger_.Log(State::kInstalling, absl::ZeroDuration(), | |
| absl::Substitute( | |
| "Package not active, proceeding with installation for: $0", | |
| package.name), | |
| LogCode::kPkgInstallProceed); | |
| for (int attempt = 0; attempt <= options_.max_retries; ++attempt) { | |
| absl::Time attempt_start_time = absl::Now(); | |
| logger_.Log( | |
| State::kInstalling, absl::ZeroDuration(), | |
| absl::Substitute("Attempting to install package: $0", package.name), | |
| LogCode::kPkgInstallStart); | |
| int return_code = ExecuteCommand(package.install_script_path); | |
| absl::Duration elapsed = absl::Now() - attempt_start_time; | |
| if (return_code == 0) { | |
| logger_.Log( | |
| State::kSuccess, elapsed, | |
| absl::Substitute("Successfully installed package: $0", package.name), | |
| LogCode::kPkgInstallSuccess); | |
| return true; | |
| } | |
| if (attempt < options_.max_retries) { | |
| logger_.Log(State::kFailureRetry, elapsed, | |
| absl::Substitute("Failed to install package: $0. Retrying.", | |
| package.name), | |
| LogCode::kPkgInstallFailRetry, attempt + 1); | |
| absl::SleepFor(absl::Seconds(2)); | |
| } else { | |
| logger_.Log( | |
| State::kFailureFatal, elapsed, | |
| absl::Substitute("Fatal error installing package: $0", package.name), | |
| LogCode::kPkgInstallFailFatal); | |
| return false; | |
| } | |
| } | |
| return false; | |
| } | |
| void Installer::ActivationCheck() { | |
| for (const auto& package : options_.packages) { | |
| // Ignore the return code of the activation script so that all packages are | |
| // checked. | |
| ExecuteCommand(package.activation_script_path); | |
| } | |
| } | |
| int Installer::ExecuteCommand(const std::vector<std::string>& args, | |
| const std::string& working_dir) { | |
| if (args.empty()) { | |
| return -1; | |
| } | |
| pid_t pid = fork(); | |
| if (pid == -1) { | |
| return -1; | |
| } | |
| if (pid == 0) { | |
| // Child process | |
| if (!working_dir.empty()) { | |
| if (chdir(working_dir.c_str()) != 0) { | |
| // Exit with 127 to mimic std::system behavior. | |
| // Use _exit() to avoid libc process cleanup behavior. | |
| _exit(127); | |
| } | |
| } | |
| std::vector<char*> argv; | |
| argv.reserve(args.size() + 1); | |
| for (const auto& arg : args) { | |
| argv.push_back(const_cast<char*>(arg.c_str())); | |
| } | |
| argv.push_back(nullptr); | |
| execvp(argv[0], argv.data()); | |
| // execvp() should not return on success. If it returns, exit child with | |
| // 127 to mimic std::system behavior. | |
| // Use _exit() to avoid libc process cleanup behavior.. | |
| _exit(127); | |
| } | |
| // Parent process | |
| int status; | |
| if (waitpid(pid, &status, 0) == -1) { | |
| return -1; | |
| } | |
| if (WIFEXITED(status)) { | |
| return WEXITSTATUS(status); | |
| } | |
| return -1; | |
| } | |
| int Installer::ExecuteCommand(const std::string& command, | |
| const std::string& working_dir) { | |
| return ExecuteCommand(absl::StrSplit(command, ' ', absl::SkipWhitespace()), | |
| working_dir); | |
| } | |
| int ActivationMain(const std::string& activation_check_dir) { | |
| std::vector<Package> packages; | |
| std::error_code ec; | |
| for (const auto& entry : | |
| std::filesystem::directory_iterator(activation_check_dir, ec)) { | |
| if (!ec && entry.is_directory() && | |
| std::filesystem::exists(entry.path() / "activation_check.sh")) { | |
| Package package; | |
| package.name = entry.path().filename().string(); | |
| package.activation_script_path = entry.path() / "activation_check.sh"; | |
| packages.push_back(package); | |
| } | |
| } | |
| InstallerOptions options; | |
| options.packages = packages; | |
| Installer installer(options); | |
| installer.ActivationCheck(); | |
| LOG(INFO) << "Activation check completed successfully."; | |
| return 0; | |
| } | |
| int SignatureCheckMain(const std::string& bundle_tar_file_path, | |
| const std::vector<std::string>& public_key_paths) { | |
| InstallerOptions options; | |
| Installer installer(options); | |
| std::filesystem::path sha256sums_file = bundle_tar_file_path; | |
| sha256sums_file.remove_filename(); | |
| sha256sums_file /= "bundle_files.SHA256SUM"; | |
| std::filesystem::path signature_file = sha256sums_file.string() + ".sig"; | |
| if (!installer.CheckBundleSignature(sha256sums_file.string(), | |
| signature_file.string(), | |
| public_key_paths)) { | |
| LOG(ERROR) << "Bundle signature check failed"; | |
| return 1; // Exit if signature verification fails | |
| } | |
| LOG(INFO) << "Bundle signature check successful."; | |
| return 0; | |
| } | |
| namespace { | |
| // Returns the post install order declared in the bundle configuration file. | |
| // The configuration file is only honored if it is listed in the bundle | |
| // manifest, as only manifest files are covered by the bundle signature check. | |
| std::vector<std::string> GetVerifiedPostInstallOrder( | |
| const std::filesystem::path& untar_dir, | |
| const absl::flat_hash_set<std::string>& manifest_files) { | |
| const std::filesystem::path config_file = | |
| untar_dir / internal::kBundleConfigFileName; | |
| const std::string manifest_entry = (std::filesystem::path("firmware_bundle") / | |
| internal::kBundleConfigFileName) | |
| .string(); | |
| if (!manifest_files.contains(manifest_entry)) { | |
| std::error_code ec; | |
| if (std::filesystem::exists(config_file, ec)) { | |
| LOG(ERROR) << "Ignoring bundle configuration file missing from the " | |
| "manifest: " | |
| << config_file; | |
| } | |
| return {}; | |
| } | |
| return internal::GetPostInstallOrder(config_file.string()); | |
| } | |
| // Sorts `packages` so that the packages named in `post_install_order` come | |
| // first, in the order they are named there, followed by the remaining packages | |
| // sorted alphabetically by name. | |
| void SortPackagesByInstallOrder( | |
| const std::vector<std::string>& post_install_order, | |
| std::vector<Package>* packages) { | |
| absl::flat_hash_map<std::string, size_t> install_rank; | |
| install_rank.reserve(post_install_order.size()); | |
| for (size_t i = 0; i < post_install_order.size(); ++i) { | |
| // On duplicates, the first occurrence defines the install order. | |
| install_rank.emplace(post_install_order[i], i); | |
| } | |
| // Packages absent from the configuration file are installed last. | |
| auto rank = [&install_rank](const Package& package) { | |
| const auto it = install_rank.find(package.name); | |
| return it == install_rank.end() ? std::numeric_limits<size_t>::max() | |
| : it->second; | |
| }; | |
| std::sort(packages->begin(), packages->end(), | |
| [&rank](const Package& a, const Package& b) { | |
| const size_t rank_a = rank(a); | |
| const size_t rank_b = rank(b); | |
| return rank_a != rank_b ? rank_a < rank_b : a.name < b.name; | |
| }); | |
| } | |
| } // namespace | |
| // Parses the firmware bundle to identify and validate software packages. | |
| // Each package must contain 'post_install.sh' and 'activation_check.sh', and | |
| // these files must be present in the manifest. | |
| // Returns a vector of valid Package objects. Packages listed in the | |
| // `post_install_order` field of the bundle configuration file come first, in | |
| // the order they are listed there. The remaining packages follow, sorted | |
| // alphabetically by package name. | |
| std::vector<Package> ParsePostInstallPackages( | |
| const std::string& tar_file_path, const std::string& sha256sums_file_path) { | |
| std::vector<Package> packages; | |
| std::filesystem::path untar_dir = tar_file_path; | |
| untar_dir.remove_filename(); | |
| untar_dir /= "firmware_bundle"; | |
| LOG(INFO) << "Firmware bundle directory: " << untar_dir; | |
| absl::flat_hash_set<std::string> manifest_files = | |
| internal::GetManifestFiles(sha256sums_file_path); | |
| std::error_code ec; | |
| for (const auto& entry : std::filesystem::directory_iterator(untar_dir, ec)) { | |
| if (ec) { | |
| continue; | |
| } | |
| if (entry.path().filename() == internal::kBundleConfigFileName) { | |
| // The bundle configuration file is not a package. | |
| continue; | |
| } | |
| if (internal::IsPackageValid(entry.path().string(), manifest_files)) { | |
| LOG(INFO) << "Found package: " << entry.path().filename().string(); | |
| Package package; | |
| package.name = entry.path().filename().string(); | |
| package.activation_script_path = entry.path() / "activation_check.sh"; | |
| package.install_script_path = entry.path() / "post_install.sh"; | |
| packages.push_back(package); | |
| } else { | |
| LOG(INFO) << "Not a valid package or missing from the manifest: " | |
| << entry.path().filename().string(); | |
| } | |
| } | |
| SortPackagesByInstallOrder( | |
| GetVerifiedPostInstallOrder(untar_dir, manifest_files), &packages); | |
| LOG(INFO) << "Post install order: " | |
| << absl::StrJoin(packages, ", ", | |
| [](std::string* out, const Package& package) { | |
| absl::StrAppend(out, package.name); | |
| }); | |
| return packages; | |
| } | |
| int PostInstallMain(const std::string& inventory_dir, | |
| const std::string& install_lock_file_path, | |
| const std::string& bundle_tar_file_path, | |
| const std::string& activation_check_dir) { | |
| InstallerOptions options; | |
| // Check if an installation is already running. | |
| std::filesystem::path install_lock_file = install_lock_file_path; | |
| if (std::error_code ec; std::filesystem::exists(install_lock_file, ec)) { | |
| LOG(ERROR) << "Installation is already in progress. Lock file found: " | |
| << install_lock_file; | |
| return 1; | |
| } | |
| std::error_code ec; | |
| if (std::filesystem::create_directories(inventory_dir, ec); ec) { | |
| LOG(ERROR) << "Error: Could not create installation directory: " | |
| << inventory_dir << " with error: " << ec.message(); | |
| return 1; | |
| } | |
| ec.clear(); | |
| if (std::filesystem::create_directories(install_lock_file.parent_path(), ec); | |
| ec) { | |
| LOG(ERROR) << "Error: Could not create installation lock file directory: " | |
| << install_lock_file.parent_path() | |
| << " with error: " << ec.message(); | |
| return 1; | |
| } | |
| // Create the installation lock file to signal that an install is starting. | |
| std::ofstream lock_file_stream(install_lock_file); | |
| if (!lock_file_stream) { | |
| LOG(ERROR) << "Error: Could not create lock file: " << install_lock_file | |
| << " with error: " << lock_file_stream.rdstate(); | |
| return 1; | |
| } | |
| absl::Cleanup lock_file_cleanup = [install_lock_file] { | |
| std::error_code ec; | |
| if (std::filesystem::remove(install_lock_file, ec); ec) { | |
| LOG(ERROR) << "Error: Could not remove lock file: " << install_lock_file; | |
| } | |
| LOG(INFO) << "Successfully removed lock file: " << install_lock_file; | |
| }; | |
| LOG(INFO) << "Successfully created lock file: " << install_lock_file; | |
| // Now run install | |
| std::filesystem::path sha256sums_file = bundle_tar_file_path; | |
| sha256sums_file.remove_filename(); | |
| sha256sums_file /= "bundle_files.SHA256SUM"; | |
| std::vector<Package> packages_to_install = | |
| ParsePostInstallPackages(bundle_tar_file_path, sha256sums_file.string()); | |
| options.packages = packages_to_install; | |
| options.max_retries = 2; | |
| std::filesystem::path log_path = inventory_dir; | |
| log_path.remove_filename(); | |
| log_path /= "progress_internal.log"; | |
| options.log_path = log_path.c_str(); | |
| options.activation_check_dir = activation_check_dir; | |
| Installer installer(options); | |
| LOG(INFO) << "Starting installation process..."; | |
| if (!installer.PostInstall()) { | |
| LOG(ERROR) << "Post install failed. Check log at " << log_path | |
| << " for details."; | |
| return 1; | |
| } | |
| LOG(INFO) << "Post install completed successfully."; | |
| return 0; | |
| } | |
| int CheckDevKeysEnabledMain(const std::string& config_file_path) { | |
| InstallerOptions options; | |
| Installer installer(options); | |
| absl::StatusOr<owner_certificate::OwnerVerificationCertConfiguration> status = | |
| installer.GetOwnerConfiguration(config_file_path); | |
| if (!status.ok()) { | |
| LOG(ERROR) << "Failed to get owner configuration: " << status.status(); | |
| LOG(ERROR) << "Dev verification keys use will be disabled by default."; | |
| return kDevKeysError; | |
| } | |
| owner_certificate::OwnerVerificationCertConfiguration config = status.value(); | |
| if (config.bundle_dev_verification_enabled()) { | |
| LOG(INFO) << "Dev verification keys are enabled via " << config_file_path; | |
| return kDevKeysEnabled; | |
| } | |
| LOG(INFO) << "Dev verification keys are explicitly disabled in " | |
| << config_file_path; | |
| return kDevKeysDisabled; | |
| } | |
| } // namespace bloom_install |