blob: b5cfacbcde51084275ee33a96206f344ab33edd2 [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "oobe_config/load_oobe_config_usb.h"
#include <pwd.h>
#include <sys/mount.h>
#include <unistd.h>
#include <map>
#include <utility>
#include <vector>
#include <base/files/file_enumerator.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/files/scoped_temp_dir.h>
#include <base/strings/string_number_conversions.h>
#include <brillo/file_utils.h>
#include <libtpmcrypto/tpm.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include "oobe_config/usb_utils.h"
using base::FilePath;
using base::ScopedTempDir;
using std::map;
using std::string;
using std::unique_ptr;
using std::vector;
namespace oobe_config {
namespace {
#if USE_TPM2
// TPMA_NV_PPWRITE | TPMA_NV_AUTHREAD | TPMA_NV_NO_DA | TPMA_NV_WRITTEN |
// TPMA_NV_PLATFORMCREATE
constexpr uint32_t kTpmPermissions = 0x62040001;
#else
// TPM_NV_PER_PPWRITE
constexpr uint32_t kTpmPermissions = 0x1;
#endif
constexpr uint32_t kHashIndexInTpmNvram = 0x100c;
// Copied from rollback_helper.cc.
// TODO(ahassani): Find a way to use the one in rollback_helper.cc
bool GetUidGid(const string& user, uid_t* uid, gid_t* gid) {
// Load the passwd entry.
constexpr int kDefaultPwnameLength = 1024;
int user_name_length = sysconf(_SC_GETPW_R_SIZE_MAX);
if (user_name_length == -1) {
user_name_length = kDefaultPwnameLength;
}
if (user_name_length < 0) {
return false;
}
passwd user_info{}, *user_infop;
vector<char> user_name_buf(static_cast<size_t>(user_name_length));
getpwnam_r(user.c_str(), &user_info, user_name_buf.data(),
static_cast<size_t>(user_name_length), &user_infop);
// NOTE: the return value can be ambiguous in the case that the user does
// not exist. See "man getpwnam_r" for details.
if (user_infop == nullptr) {
return false;
}
*uid = user_info.pw_uid;
*gid = user_info.pw_gid;
return true;
}
} // namespace
unique_ptr<LoadOobeConfigUsb> LoadOobeConfigUsb::CreateInstance() {
return std::make_unique<LoadOobeConfigUsb>(
FilePath(kStatefulDir), FilePath(kDevDiskById), FilePath(kStoreDir));
}
LoadOobeConfigUsb::LoadOobeConfigUsb(const FilePath& stateful_dir,
const FilePath& device_ids_dir,
const FilePath& store_dir)
: stateful_(stateful_dir),
device_ids_dir_(device_ids_dir),
store_dir_(store_dir),
public_key_(nullptr) {
unencrypted_oobe_config_dir_ = stateful_.Append(kUnencryptedOobeConfigDir);
pub_key_file_ = unencrypted_oobe_config_dir_.Append(kKeyFile);
config_signature_file_ =
unencrypted_oobe_config_dir_.Append(kConfigFile).AddExtension("sig");
enrollment_domain_signature_file_ =
unencrypted_oobe_config_dir_.Append(kDomainFile).AddExtension("sig");
usb_device_path_signature_file_ =
unencrypted_oobe_config_dir_.Append(kUsbDevicePathSigFile);
}
bool LoadOobeConfigUsb::ReadFiles() {
if (!base::PathExists(stateful_)) {
LOG(ERROR) << "Stateful partition's path " << stateful_.value()
<< " does not exist.";
return false;
}
if (!base::PathExists(unencrypted_oobe_config_dir_)) {
LOG(WARNING) << "oobe_config directory on stateful partition "
<< unencrypted_oobe_config_dir_.value()
<< " does not exist. This is not an error if the system is not"
<< " configured for auto oobe.";
return false;
}
if (!base::PathExists(pub_key_file_)) {
LOG(WARNING) << "Public key file " << pub_key_file_.value()
<< " does not exist. ";
return false;
}
if (!ReadPublicKey(pub_key_file_, &public_key_)) {
return false;
}
map<FilePath, string*> signature_files = {
{config_signature_file_, &config_signature_},
{enrollment_domain_signature_file_, &enrollment_domain_signature_},
{usb_device_path_signature_file_, &usb_device_path_signature_}};
for (auto& file : signature_files) {
if (!base::PathExists(file.first)) {
LOG(WARNING) << "File " << file.first.value() << " does not exist. ";
return false;
}
if (!base::ReadFileToString(file.first, file.second)) {
PLOG(ERROR) << "Failed to read file: " << file.first.value();
return false;
}
}
return true;
}
bool LoadOobeConfigUsb::VerifyPublicKey() {
std::unique_ptr<tpmcrypto::Tpm> tpm_crypto = tpmcrypto::CreateTpmInstance();
uint32_t attributes = 0;
if (!tpm_crypto->GetNVAttributes(kHashIndexInTpmNvram, &attributes)) {
LOG(ERROR) << "Failed to get NV attributes.";
return false;
}
if (attributes != kTpmPermissions) {
LOG(ERROR) << "Attributes given (" << attributes
<< ") does not match the expected (" << kTpmPermissions << ").";
return false;
}
string hash_from_tpm;
if (!tpm_crypto->NVReadNoAuth(kHashIndexInTpmNvram, 0 /* offset */,
SHA256_DIGEST_LENGTH, &hash_from_tpm)) {
LOG(ERROR) << "Failed to read the hash value from TPM.";
return false;
}
if (hash_from_tpm.size() != SHA256_DIGEST_LENGTH) {
LOG(ERROR) << "NVReadNoAuth() returned data with size "
<< hash_from_tpm.size() << " != " << SHA256_DIGEST_LENGTH;
return false;
}
string public_key_content;
if (!base::ReadFileToString(pub_key_file_, &public_key_content)) {
PLOG(ERROR) << "Failed to read the public key: " << pub_key_file_.value();
return false;
}
// Calculating the hash of the public key.
char hash_from_public_key[SHA256_DIGEST_LENGTH];
auto address = reinterpret_cast<unsigned char*>(hash_from_public_key);
if (SHA256(reinterpret_cast<const unsigned char*>(public_key_content.c_str()),
public_key_content.length(), address) != address) {
LOG(ERROR) << "openssl returned an invalid hash pointer!";
return false;
}
// Finally, compare the two hashes to see if they are equal.
if (string(hash_from_public_key, SHA256_DIGEST_LENGTH) != hash_from_tpm) {
LOG(ERROR) << "Public key hash ("
<< base::HexEncode(hash_from_public_key, SHA256_DIGEST_LENGTH)
<< ") does not match the hash in the TPM ("
<< base::HexEncode(hash_from_tpm.data(), SHA256_DIGEST_LENGTH)
<< ")";
return false;
}
return true;
}
bool LoadOobeConfigUsb::LocateUsbDevice(FilePath* device_id) {
// /stateful/unencrypted/oobe_auto_config/usb_device_path.sig is the signature
// of a /dev/disk/by-id path for the root USB device (e.g. /dev/sda). So to
// obtain which USB we were on pre-reboot:
// for dev in /dev/disk/by-id/ *:
// if dev verifies with usb_device_path.sig with validation_key.pub:
// USB block device is at readlink(dev)
base::FileEnumerator iter(device_ids_dir_, false,
base::FileEnumerator::FILES);
for (auto link = iter.Next(); !link.empty(); link = iter.Next()) {
if (VerifySignature(link.value(), usb_device_path_signature_,
public_key_) &&
base::NormalizeFilePath(link, device_id)) {
LOG(INFO) << "Found USB device " << device_id->value();
return true;
}
}
LOG(ERROR) << "Did not find the USB device. Probably it was taken out?";
return false;
}
bool LoadOobeConfigUsb::MountUsbDevice(const FilePath& device_path,
const FilePath& mount_point) {
LOG(INFO) << "Mounting " << device_path.value() << " on "
<< mount_point.value();
auto kMountFlags = MS_RDONLY | MS_NOEXEC | MS_NOSUID | MS_NODEV;
if (mount(device_path.value().c_str(), mount_point.value().c_str(), "ext4",
kMountFlags, nullptr) != 0) {
PLOG(ERROR) << "Failed to mount " << device_path.value() << "on "
<< mount_point.value();
return false;
}
return true;
}
bool LoadOobeConfigUsb::UnmountUsbDevice(const FilePath& mount_point) {
LOG(INFO) << "Unmounting " << mount_point.value();
if (umount(mount_point.value().c_str()) != 0) {
PLOG(WARNING) << "Failed to unmount " << mount_point.value();
return false;
}
return true;
}
bool LoadOobeConfigUsb::Load() {
if (!ReadFiles()) {
return false;
}
if (!VerifyPublicKey()) {
return false;
}
// By now we have all the files necessary on the stateful partition. Now we
// have to look into the USB drives.
FilePath device_path;
if (!LocateUsbDevice(&device_path)) {
return false;
}
ScopedTempDir usb_mount_path;
if (!usb_mount_path.CreateUniqueTempDir() ||
!MountUsbDevice(device_path, usb_mount_path.GetPath())) {
return false;
}
// /stateful/unencrypted/oobe_auto_config/config.json.sig is the signature of
// the config.json file on the USB stateful.
FilePath unencrypted_oobe_config_dir_on_usb =
usb_mount_path.GetPath().Append(kUnencryptedOobeConfigDir);
FilePath config_file_on_usb =
unencrypted_oobe_config_dir_on_usb.Append(kConfigFile);
if (!base::ReadFileToString(config_file_on_usb, &config_)) {
LOG(ERROR) << "Failed to read oobe config file: "
<< config_file_on_usb.value();
return false;
}
if (!VerifySignature(config_, config_signature_, public_key_)) {
return false;
}
// /stateful/unencrypted/oobe_auto_config/enrollment_domain.sig is the
// signature of the enrollment_domain file on the USB stateful.
FilePath enrollment_domain_file_on_usb =
unencrypted_oobe_config_dir_on_usb.Append(kDomainFile);
if (!base::ReadFileToString(enrollment_domain_file_on_usb,
&enrollment_domain_)) {
LOG(ERROR) << "Failed to read enrollment domain file: "
<< enrollment_domain_file_on_usb.value();
return false;
}
if (!VerifySignature(enrollment_domain_, enrollment_domain_signature_,
public_key_)) {
return false;
}
// Ignore the failure.
UnmountUsbDevice(usb_mount_path.GetPath());
return true;
}
bool LoadOobeConfigUsb::Store() {
if (!Load()) {
return false;
}
// Find the GID/UID of oobe_config_restore.
uid_t uid;
gid_t gid;
if (!GetUidGid(kOobeConfigRestoreUser, &uid, &gid)) {
PLOG(ERROR) << "Failed to get the UID/GID for " << kOobeConfigRestoreUser;
return false;
}
map<FilePath, string*> files = {
{store_dir_.Append(kConfigFile), &config_},
{store_dir_.Append(kDomainFile), &enrollment_domain_}};
for (const auto& file : files) {
if (!brillo::WriteStringToFile(file.first, *file.second)) {
PLOG(ERROR) << "Failed to write the config to " << file.first.value();
return false;
}
// Change owners to oobe_config_restore.
if (lchown(file.first.value().c_str(), uid, gid) != 0) {
PLOG(ERROR) << "Couldn't change ownership of " << file.first.value()
<< " to " << kOobeConfigRestoreUser;
return false;
}
}
return true;
}
bool LoadOobeConfigUsb::GetOobeConfigJson(string* config,
string* enrollment_domain) {
CHECK(config);
CHECK(enrollment_domain);
auto config_file = store_dir_.Append(kConfigFile);
if (!base::ReadFileToString(config_file, config)) {
PLOG(ERROR) << "Failed to read in the config file: " << config_file.value();
return false;
}
auto enrollment_domain_file = store_dir_.Append(kDomainFile);
if (!base::ReadFileToString(enrollment_domain_file, enrollment_domain)) {
PLOG(ERROR) << "Failed to read in the enrollment_domain file: "
<< enrollment_domain_file.value();
return false;
}
return true;
}
void LoadOobeConfigUsb::CleanupFilesOnDevice() {
if (!base::DirectoryExists(unencrypted_oobe_config_dir_)) {
return;
}
if (!base::DeleteFile(unencrypted_oobe_config_dir_, true)) {
LOG(ERROR) << "Failed to delete directory "
<< unencrypted_oobe_config_dir_.value();
}
}
} // namespace oobe_config