| // Copyright 2022 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "init/startup/security_manager.h" | |
| #include <fcntl.h> | |
| #include <sys/ioctl.h> | |
| #include <iostream> | |
| #include <memory> | |
| #include <string> | |
| #include <vector> | |
| #include <base/files/file_enumerator.h> | |
| #include <base/files/file_path.h> | |
| #include <base/files/file_util.h> | |
| #include <base/logging.h> | |
| #include <base/rand_util.h> | |
| #include <base/strings/string_split.h> | |
| #include <base/strings/string_util.h> | |
| #include <libstorage/platform/platform.h> | |
| #include <linux/loadpin.h> | |
| #include <openssl/sha.h> | |
| #include "init/startup/startup_dep_impl.h" | |
| namespace { | |
| constexpr char kSysKernelSecurity[] = "sys/kernel/security"; | |
| constexpr char kDevNull[] = "dev/null"; | |
| constexpr char kLoadPinVerity[] = "loadpin/dm-verity"; | |
| // During CrOS build phases, this file will be produced and baked into the | |
| // rootfs. Specifically during the DLC build flows. | |
| constexpr char kTrustedDlcVerityDigests[] = | |
| "opt/google/dlc/_trusted_verity_digests"; | |
| constexpr char kProcessMgmtPoliciesDir[] = | |
| "usr/share/cros/startup/process_management_policies"; | |
| constexpr char kSafeSetIDProcessMgmtPolicies[] = "safesetid"; | |
| constexpr char kLsmInodePolicies[] = | |
| "sys/kernel/security/chromiumos/inode_security_policies"; | |
| constexpr char kNoEarlyKeyFile[] = ".no_early_system_key"; | |
| constexpr char kSysKeyBackupFile[] = "unencrypted/preserve/system.key"; | |
| constexpr int kKeySize = SHA256_DIGEST_LENGTH; | |
| const std::array<const char*, 3> kSymlinkExceptions = { | |
| "var/lib/timezone", | |
| "var/log", | |
| "home", | |
| }; | |
| constexpr char kSymlinkExceptionsDir[] = | |
| "usr/share/cros/startup/symlink_exceptions"; | |
| constexpr char kFifoExceptionsDir[] = "usr/share/cros/startup/fifo_exceptions"; | |
| constexpr char kVar[] = "var"; | |
| } // namespace | |
| namespace startup { | |
| // Project-specific process management policies. Projects may add policies by | |
| // adding a file under usr/share/cros/startup/process_management_policies/ | |
| // for UID's, whose contents are one or more lines specifying a parent ID | |
| // and a child UID that the parent can use for the purposes of process | |
| // management. There should be one line for every mapping that is to be put in | |
| // the allow list. Lines in the file should use the following format: | |
| // <UID>:<UID>. | |
| // | |
| // For example, if the 'shill' user needs to use 'dhcp', 'openvpn' and 'ipsec' | |
| // and 'syslog' for process management, the file would look like: | |
| // 20104:224 | |
| // 20104:217 | |
| // 20104:212 | |
| // 20104:202 | |
| // | |
| // AccumulatePolicyFiles takes in all the files contained in the policy_dir | |
| // reads their contents, copies and appends them to a file determined by | |
| // output_file. | |
| bool AccumulatePolicyFiles(libstorage::Platform* platform, | |
| const base::FilePath& root, | |
| const base::FilePath& output_file, | |
| const base::FilePath& policy_dir) { | |
| if (!platform->FileExists(output_file)) { | |
| // securityfs files are located elsewhere, return. | |
| return true; | |
| } | |
| if (!platform->DirectoryExists(policy_dir)) { | |
| LOG(WARNING) << "Can't configure process management security. " | |
| << policy_dir << " not found."; | |
| return false; | |
| } | |
| std::unique_ptr<libstorage::FileEnumerator> enumerator( | |
| platform->GetFileEnumerator(policy_dir, false, | |
| base::FileEnumerator::FileType::FILES)); | |
| std::vector<std::string> combined_policy; | |
| for (base::FilePath file = enumerator->Next(); !file.empty(); | |
| file = enumerator->Next()) { | |
| std::string file_str; | |
| DLOG(INFO) << "Loading: " << file.value(); | |
| if (!platform->ReadFileToString(file, &file_str)) { | |
| PLOG(WARNING) << "Can't read policy file " << file; | |
| continue; | |
| } | |
| std::vector<std::string> split_files = base::SplitString( | |
| file_str, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); | |
| split_files.erase(std::remove_if(split_files.begin(), split_files.end(), | |
| [&](const std::string line) { | |
| return base::StartsWith(line, "#"); | |
| }), | |
| split_files.end()); | |
| combined_policy.push_back(base::JoinString(split_files, "\n")); | |
| } | |
| std::string combined_policy_str = base::JoinString(combined_policy, "\n"); | |
| combined_policy_str.append("\n"); | |
| DLOG(INFO) << "Applying policy to: " << output_file.value(); | |
| if (!platform->WriteStringToFile(output_file, combined_policy_str)) { | |
| PLOG(ERROR) << output_file << ": Failed to write to file"; | |
| return false; | |
| } | |
| return true; | |
| } | |
| // Determine where securityfs files are placed. | |
| // No inputs, checks for which securityfs file paths exist | |
| // and accumulates files for securityfs. | |
| bool ConfigureProcessMgmtSecurity(libstorage::Platform* platform, | |
| const base::FilePath& root) { | |
| DLOG(INFO) << "ConfigureProcessMgmtSecurity"; | |
| // For UID relevant files. | |
| const base::FilePath policies_dir = | |
| root.Append(kSysKernelSecurity).Append(kSafeSetIDProcessMgmtPolicies); | |
| // Path to the securityfs file for configuring process management security | |
| // policies, for UIDs, in the SafeSetID LSM (used for kernel version >= 5.9). | |
| const base::FilePath uid_mgmt_policies = | |
| policies_dir.Append("uid_allowlist_policy"); | |
| // Path to the securityfs file for configuring process management security | |
| // policies in the SafeSetID LSM (used for kernel version >= 4.14) | |
| const base::FilePath mgmt_policies = policies_dir.Append("whitelist_policy"); | |
| const base::FilePath pmpd = root.Append(kProcessMgmtPoliciesDir); | |
| return AccumulatePolicyFiles(platform, root, uid_mgmt_policies, pmpd) && | |
| AccumulatePolicyFiles(platform, root, mgmt_policies, pmpd); | |
| } | |
| bool SetupLoadPinVerityDigests(libstorage::Platform* platform, | |
| const base::FilePath& root, | |
| StartupDep* startup_dep) { | |
| const auto loadpin_verity = | |
| root.Append(kSysKernelSecurity).Append(kLoadPinVerity); | |
| const auto trusted_dlc_digests = root.Append(kTrustedDlcVerityDigests); | |
| const auto dev_null = root.Append(kDevNull); | |
| // Only try loading the trusted dm-verity root digests if: | |
| // 1. LoadPin dm-verity attribute is supported. | |
| // 2a. Trusted list of DLC dm-verity root digest file exists. | |
| // 2b. Otherwise, we must feed LoadPin with an invalid digests file. | |
| // Open (write) the LoadPin dm-verity attribute file. | |
| if (!platform->FileExists(loadpin_verity)) { | |
| // This means LoadPin dm-verity attribute is not supported. | |
| // No further action is required. | |
| return true; | |
| } | |
| FILE* fd = platform->OpenFile(loadpin_verity, "w"); | |
| if (!fd) { | |
| // TODO(kimjae): Need to somehow handle this failure, as this still means | |
| // later a digest can get fed into LoadPin. | |
| PLOG(ERROR) << "Failed to open LoadPin verity file."; | |
| return false; | |
| } | |
| // Open (read) the trusted digest file in rootfs. | |
| FILE* digests_fd = platform->OpenFile(trusted_dlc_digests, "r"); | |
| if (!digests_fd) { | |
| if (errno == ENOENT) { | |
| PLOG(WARNING) << "Missing trusted DLC verity digests file."; | |
| // NOTE: Do not return here, so invalid digests get fed into LoadPin. | |
| } else { | |
| PLOG(WARNING) << "Failed to open trusted DLC verity digests file."; | |
| // NOTE: Do not return here, so invalid digests get fed into LoadPin. | |
| } | |
| // Any failure in loading/parsing will block subsequent feeds into LoadPin. | |
| digests_fd = platform->OpenFile(dev_null, "r"); | |
| if (!digests_fd) { | |
| PLOG(ERROR) << "Failed to open " << dev_null.value() << "."; | |
| platform->CloseFile(fd); | |
| return false; | |
| } | |
| LOG(WARNING) << "Forcing LoadPin to ingest /dev/null."; | |
| } | |
| // Write trusted digests or /dev/null into LoadPin. | |
| int arg1 = fileno(digests_fd); | |
| int ret = platform->Ioctl(fd, LOADPIN_IOC_SET_TRUSTED_VERITY_DIGESTS, &arg1); | |
| if (ret != 0) { | |
| PLOG(WARNING) << "Unable to setup trusted DLC verity digests"; | |
| } | |
| // On success or failure: | |
| // Subsequent `ioctl` on loadpin/dm-verity should fail as the trusted | |
| // dm-verity root digest list is not empty or invalid digest file descriptor | |
| // is fed into LoadPin. | |
| platform->CloseFile(fd); | |
| platform->CloseFile(digests_fd); | |
| return ret == 0; | |
| } | |
| bool BlockSymlinkAndFifo(libstorage::Platform* platform, | |
| const base::FilePath& root, | |
| const std::string& path) { | |
| base::FilePath base = root.Append(kLsmInodePolicies); | |
| base::FilePath sym = base.Append("block_symlink"); | |
| base::FilePath fifo = base.Append("block_fifo"); | |
| bool ret = true; | |
| if (!platform->WriteStringToFile(sym, path)) { | |
| PLOG(WARNING) << "Failed to write to block_symlink for " << path; | |
| ret = false; | |
| } | |
| if (!platform->WriteStringToFile(fifo, path)) { | |
| PLOG(WARNING) << "Failed to write to block_fifo for " << path; | |
| ret = false; | |
| } | |
| return ret; | |
| } | |
| // Generates a system key in test images, before the normal mount-encrypted. | |
| // This allows us to soft-clear the TPM in integration tests w/o accidentally | |
| // wiping encstateful after a reboot. | |
| void CreateSystemKey(libstorage::Platform* platform, | |
| const base::FilePath& root, | |
| const base::FilePath& stateful, | |
| StartupDep* startup_dep, | |
| std::string* log_content) { | |
| base::FilePath no_early = stateful.Append(kNoEarlyKeyFile); | |
| base::FilePath backup = stateful.Append(kSysKeyBackupFile); | |
| base::FilePath empty; | |
| if (platform->FileExists(no_early)) { | |
| log_content->append("Opt not to create a system key in advance."); | |
| return; | |
| } | |
| log_content->append("Checking if a system key already exists in NVRAM...\n"); | |
| std::string output; | |
| std::vector<std::string> mnt_enc_info = {"info"}; | |
| if (!startup_dep->MountEncrypted(mnt_enc_info, &output)) { | |
| log_content->append(output); | |
| log_content->append("\n"); | |
| if (output.find("NVRAM: available.") != std::string::npos) { | |
| log_content->append("There is already a system key in NVRAM.\n"); | |
| return; | |
| } | |
| } | |
| log_content->append("No system key found in NVRAM. Start creating one.\n"); | |
| // Generates 32-byte random key material and backs it up. | |
| brillo::Blob buf(kKeySize); | |
| base::RandBytes(buf); | |
| if (!platform->WriteFile(backup, buf)) { | |
| log_content->append("Failed to generate or back up system key material.\n"); | |
| return; | |
| } | |
| // Persists system key. | |
| std::vector<std::string> mnt_enc_set = {"set", backup.value()}; | |
| if (!startup_dep->MountEncrypted(mnt_enc_set, &output)) { | |
| log_content->append(output); | |
| log_content->append("Successfully created a system key."); | |
| } | |
| } | |
| bool AllowSymlink(libstorage::Platform* platform, | |
| const base::FilePath& root, | |
| const std::string& path) { | |
| base::FilePath sym = root.Append(kLsmInodePolicies).Append("allow_symlink"); | |
| return platform->WriteStringToFile(sym, path); | |
| } | |
| bool AllowFifo(libstorage::Platform* platform, | |
| const base::FilePath& root, | |
| const std::string& path) { | |
| base::FilePath fifo = root.Append(kLsmInodePolicies).Append("allow_fifo"); | |
| return platform->WriteStringToFile(fifo, path); | |
| } | |
| void SymlinkExceptions(libstorage::Platform* platform, | |
| const base::FilePath& root) { | |
| // Generic symlink exceptions. | |
| for (auto d_it = kSymlinkExceptions.begin(); d_it != kSymlinkExceptions.end(); | |
| d_it++) { | |
| base::FilePath d = root.Append(*d_it); | |
| if (!platform->CreateDirectory(d)) { | |
| PLOG(WARNING) << "mkdir failed for " << d.value(); | |
| } | |
| if (!platform->SetPermissions(d, 0755)) { | |
| PLOG(WARNING) << "Failed to set permissions for " << d.value(); | |
| } | |
| AllowSymlink(platform, root, d.value()); | |
| } | |
| } | |
| // Project-specific exceptions. Projects may add exceptions by | |
| // adding a file under excepts_dir whose contents contains a list | |
| // of paths (one per line) for which an exception should be made. | |
| // File name should use the following format: | |
| // <project-name>-{symlink|fifo}-exceptions.txt | |
| void ExceptionsProjectSpecific(libstorage::Platform* platform, | |
| const base::FilePath& root, | |
| const base::FilePath& config_dir, | |
| bool (*callback)(libstorage::Platform* platform, | |
| const base::FilePath& root, | |
| const std::string& path)) { | |
| if (platform->DirectoryExists(config_dir)) { | |
| std::unique_ptr<libstorage::FileEnumerator> iter( | |
| platform->GetFileEnumerator(config_dir, false, | |
| base::FileEnumerator::FileType::FILES)); | |
| for (base::FilePath path_file = iter->Next(); !path_file.empty(); | |
| path_file = iter->Next()) { | |
| if (!platform->FileExists(path_file)) { | |
| continue; | |
| } | |
| std::string contents; | |
| if (!platform->ReadFileToString(path_file, &contents)) { | |
| PLOG(WARNING) << "Can't open exceptions file " << path_file.value(); | |
| continue; | |
| } | |
| std::vector<std::string> files = base::SplitString( | |
| contents, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); | |
| for (const auto& path : files) { | |
| if (path.find("#") == 0) { | |
| continue; | |
| } else { | |
| base::FilePath p(path); | |
| if (!platform->CreateDirectory(p)) { | |
| PLOG(WARNING) << "mkdir failed for " << path; | |
| } | |
| if (!platform->SetPermissions(p, 0755)) { | |
| PLOG(WARNING) << "Failed to set permissions for " << path; | |
| } | |
| callback(platform, root, path); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Set up symlink traversal and FIFO blocking policy, and project | |
| // specific symlink and FIFO exceptions. | |
| void ConfigureFilesystemExceptions(libstorage::Platform* platform, | |
| const base::FilePath& root) { | |
| // Set up symlink traversal and FIFO blocking policy for /var, which may | |
| // reside on a separate file system than /mnt/stateful_partition. Block | |
| // symlink traversal and opening of FIFOs by default, but allow exceptions | |
| // in the few instances where they are used intentionally. | |
| BlockSymlinkAndFifo(platform, root, root.Append(kVar).value()); | |
| SymlinkExceptions(platform, root); | |
| // Project-specific symlink exceptions. Projects may add exceptions by | |
| // adding a file under /usr/share/cros/startup/symlink_exceptions/ whose | |
| // contents contains a list of paths (one per line) for which an exception | |
| // should be made. File name should use the following format: | |
| // <project-name>-symlink-exceptions.txt | |
| base::FilePath sym_excepts = root.Append(kSymlinkExceptionsDir); | |
| ExceptionsProjectSpecific(platform, root, sym_excepts, &AllowSymlink); | |
| // Project-specific FIFO exceptions. Projects may add exceptions by adding | |
| // a file under /usr/share/cros/startup/fifo_exceptions/ whose contents | |
| // contains a list of paths (one per line) for which an exception should be | |
| // made. File name should use the following format: | |
| // <project-name>-fifo-exceptions.txt | |
| base::FilePath fifo_excepts = root.Append(kFifoExceptionsDir); | |
| ExceptionsProjectSpecific(platform, root, fifo_excepts, &AllowFifo); | |
| } | |
| } // namespace startup |