blob: d25a887fa31f8079b5365ab9fb2f900d40178783 [file] [log] [blame]
// 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 "featured/store_impl.h"
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include <base/files/file.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <brillo/secure_blob.h>
#include <brillo/files/file_util.h>
#include <brillo/secure_string.h>
#include <featured/proto_bindings/featured.pb.h>
#include <libhwsec-foundation/crypto/hmac.h>
#include <sys/stat.h>
namespace featured {
constexpr char kStorePath[] = "/var/lib/featured/store";
// No longer used; this constant is only here to clean up older usage of this
// file.
constexpr char kStoreHMACPath[] = "/var/lib/featured/store_hmac";
constexpr mode_t kSystemFeaturedFilesMode = 0760;
constexpr size_t kTpmSeedSize = 32;
// File where the TPM seed is stored, that we have to read from.
constexpr char kTpmSeedTmpFile[] = "/run/featured_seed/tpm_seed";
namespace {
// Walks the directory tree to make sure we avoid symlinks.
// Creates |path| if it does not exist.
//
// All parent parts must already exist else we return false.
bool ValidatePathAndOpen(const base::FilePath& path,
int* outfd,
int flags = 0) {
std::vector<std::string> components = path.GetComponents();
if (components.empty()) {
LOG(ERROR) << "Cannot open an empty path";
return false;
}
int parentfd = AT_FDCWD;
for (auto it = components.begin(); it != components.end(); ++it) {
std::string component = *it;
int fd;
if (it == components.end() - 1) {
// Check that the last component is a valid file and open it for reading
// and writing.
fd = openat(parentfd, component.c_str(),
O_CREAT | O_RDWR | O_NOFOLLOW | O_CLOEXEC | flags,
kSystemFeaturedFilesMode);
} else {
// Check that all components except the last are a valid directory.
fd = openat(parentfd, component.c_str(),
O_NOFOLLOW | O_CLOEXEC | O_PATH | O_DIRECTORY);
}
if (fd < 0) {
PLOG(ERROR) << "Unable to access path: " << path.value() << " ("
<< component << ")";
if (parentfd != AT_FDCWD) {
close(parentfd);
}
return false;
}
if (parentfd != AT_FDCWD) {
close(parentfd);
}
parentfd = fd;
}
*outfd = parentfd;
return true;
}
// Validates |file_path| according to |ValidatePathAndOpen| and reads the
// contents into |file_content|. Creates |file_path| if it does not exist.
//
// Returns false if validating, opening, or reading |file_path| fail.
//
// NOTE: While |file_path| could be recreated if reading fails, doing so is
// risky since deletion could have unintended consequences (eg. the file is a
// symlink).
bool ValidatePathAndRead(const base::FilePath& file_path,
std::string& file_content) {
int fd;
if (!ValidatePathAndOpen(file_path, &fd)) {
LOG(ERROR) << "Failed to validate and open " << file_path;
return false;
} else {
// Constructing with |fd| instead of |file_path| to avoid potential
// TOCTOU (time-of-check/time-of-use) vulnerabilities between calling
// |ValidatePathAndOpen| and constructing |file|.
base::File file(fd);
std::vector<uint8_t> buffer(file.GetLength());
if (!file.ReadAndCheck(/*offset=*/0, base::make_span(buffer))) {
LOG(ERROR) << "Failed to read file contents";
return false;
}
file_content = std::string(buffer.begin(), buffer.end());
}
return true;
}
// Overwrite the file's contents before deleting, to ensure data is wiped.
void SafeDeleteFile(const base::FilePath& seed_path) {
brillo::Blob all_zero(kTpmSeedSize);
if (!base::WriteFile(seed_path, all_zero)) {
PLOG(WARNING) << "Failed to write zeroes to the TPM seed file.";
}
if (!brillo::DeleteFile(seed_path)) {
PLOG(WARNING) << "Failed to delete the TPM seed file.";
}
}
std::optional<brillo::SecureBlob> GetTpmSeed(const base::FilePath& seed_path) {
brillo::SecureBlob tpm_seed(kTpmSeedSize);
int bytes_read = base::ReadFile(
seed_path, reinterpret_cast<char*>(tpm_seed.data()), tpm_seed.size());
SafeDeleteFile(seed_path);
if (bytes_read != kTpmSeedSize) {
LOG(ERROR) << "Failed to read TPM seed from tmpfile, size expected: "
<< kTpmSeedSize << ", size got: " << bytes_read << ".";
return std::nullopt;
}
return tpm_seed;
}
} // namespace
StoreImpl::StoreImpl(const Store& store,
const base::FilePath& store_path,
std::optional<brillo::SecureBlob>&& tpm_seed,
const OverridesSet& overrides)
: store_(store),
store_path_(store_path),
tpm_seed_(tpm_seed),
overrides_(overrides) {}
void StoreImpl::ComputeHMACAndUpdate() {
if (!tpm_seed_.has_value()) {
LOG(WARNING) << "Couldn't compute HMAC because there's no key; continuing.";
return;
}
brillo::SecureBlob hash = hwsec_foundation::HmacSha256(
tpm_seed_.value(), brillo::BlobFromString(store_.overrides()));
store_.set_overrides_hmac(hash.to_string());
}
// Updates hmac, writes store and hmac to disk.
bool StoreImpl::WriteDisk() {
ComputeHMACAndUpdate();
std::string serialized_store;
bool serialized = store_.SerializeToString(&serialized_store);
if (!serialized) {
LOG(ERROR) << "Could not serialize protobuf";
return false;
}
int store_fd;
if (!ValidatePathAndOpen(store_path_, &store_fd, O_TRUNC)) {
PLOG(ERROR) << "Could not reopen " << store_path_;
return false;
}
// Write store to disk.
base::File store_file(store_fd);
if (!store_file.WriteAtCurrentPosAndCheck(
base::as_bytes(base::make_span(serialized_store)))) {
PLOG(ERROR) << "Could not write new store to disk";
return false;
}
return true;
}
std::unique_ptr<StoreInterface> StoreImpl::Create() {
if (base::PathExists(base::FilePath(kStoreHMACPath))) {
// Clean up after oureselves from prior implementation.
if (!brillo::DeleteFile(base::FilePath(kStoreHMACPath))) {
PLOG(ERROR) << "Failed to delete HMAC file";
}
}
return Create(base::FilePath(kStorePath), base::FilePath(kTpmSeedTmpFile));
}
std::unique_ptr<StoreInterface> StoreImpl::Create(
base::FilePath store_path, base::FilePath tpm_seed_path) {
// Do this first so that we always clean up the seed.
std::optional<brillo::SecureBlob> tpm_seed = GetTpmSeed(tpm_seed_path);
if (!tpm_seed.has_value()) {
LOG(ERROR) << "Failed to get TPM seed. Overrides updates will fail.";
}
// Read the store.
// Open store file or create if it does not exist.
std::string store_content;
if (!ValidatePathAndRead(store_path, store_content)) {
LOG(ERROR) << "Failed to validate and read from " << store_path;
return nullptr;
}
// Deserialize the proto and store it in memory.
Store store;
bool deserialized_store = store.ParseFromString(store_content);
bool write_back = false;
if (!deserialized_store) {
LOG(ERROR) << "Failed to deserialize store";
store.Clear();
// Write the cleared store back to disk.
write_back = true;
}
// Verify the HMAC, falling back to no overrides if it fails to verify
// (or is missing).
brillo::SecureBlob hash;
if (tpm_seed.has_value()) {
// Only attempt to compute the hash if there is a seed. (The seed won't be
// available if featured crashes and restarts -- see GetTmpSeed().)
hash = hwsec_foundation::HmacSha256(
tpm_seed.value(), brillo::BlobFromString(store.overrides()));
}
// Mark as verified only if:
// 1) There is a tpm_seed
// 2) The stored HMAC in the proto has the right length
// 3) The HMAC match.
bool verified =
hash.size() == store.overrides_hmac().size() &&
(brillo::SecureMemcmp(hash.data(), store.overrides_hmac().data(),
hash.size())) == 0;
if (!verified) {
if (!store.overrides().empty()) {
// If the hash fails *and* there were overrides, reset them so that we
// don't use them this run of featured.
LOG(ERROR)
<< "HMAC verification failed; falling back to default overrides";
store.clear_overrides();
store.clear_overrides_hmac();
if (tpm_seed.has_value()) {
// Write cleared state back to disk to reset any corruption / reset any
// attacker-modified state.
// Even though we cleared overrides_hmac, WriteDisk will recompute it.
// *ONLY* do this if there was a seed; otherwise it's most likely that
// featured crashed and restarted, and we shouldn't make destructive
// changes.
// (featured deletes the seed immediately after reading it to prevent
// malicious processes from reading it; see GetTpmSeed above.)
write_back = true;
}
}
}
OverridesSet overrides;
if (!overrides.ParseFromString(store.overrides())) {
LOG(ERROR) << "Overrides deserialization failed; falling back to default "
"overrides";
store.clear_overrides();
store.clear_overrides_hmac();
write_back = true;
}
auto store_impl = std::unique_ptr<StoreImpl>(
new StoreImpl(store, store_path, std::move(tpm_seed), overrides));
if (write_back) {
if (!store_impl->WriteDisk()) {
// Fail more quickly, since, if writing fails, the class won't be able to
// provide any mutating functions.
return nullptr;
}
}
return store_impl;
}
uint32_t StoreImpl::GetBootAttemptsSinceLastUpdate() {
return store_.boot_attempts_since_last_seed_update();
}
bool StoreImpl::IncrementBootAttemptsSinceLastUpdate() {
uint32_t boot_attempts = GetBootAttemptsSinceLastUpdate();
store_.set_boot_attempts_since_last_seed_update(boot_attempts + 1);
if (!WriteDisk()) {
LOG(ERROR) << "Failed to increment boot attempts to disk.";
return false;
}
return true;
}
bool StoreImpl::ClearBootAttemptsSinceLastUpdate() {
store_.set_boot_attempts_since_last_seed_update(0);
if (!WriteDisk()) {
LOG(ERROR) << "Failed to increment boot attempts to disk.";
return false;
}
return true;
}
SeedDetails StoreImpl::GetLastGoodSeed() {
return store_.last_good_seed();
}
bool StoreImpl::SetLastGoodSeed(const SeedDetails& seed) {
*store_.mutable_last_good_seed() = seed;
if (!WriteDisk()) {
LOG(ERROR) << "Failed to increment boot attempts to disk.";
return false;
}
return true;
}
// TODO(kendraketsui): implement.
std::vector<FeatureOverride> StoreImpl::GetOverrides() {
return std::vector<FeatureOverride>();
}
// TODO(kendraketsui): implement.
void StoreImpl::AddOverride(const FeatureOverride& override) {}
// TODO(kendraketsui): implement.
void StoreImpl::RemoveOverrideFor(const std::string& name) {}
} // namespace featured