blob: 578d555f30e3e56ca4d9abd0ab29631418e759b3 [file] [log] [blame]
// Copyright 2016 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 "imageloader_impl.h"
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sys/types.h>
#include <sys/vfs.h>
#include <unistd.h>
#include </usr/include/linux/magic.h>
#include <sstream>
#include <string>
#include <utility>
#include <base/command_line.h>
#include <base/containers/adapters.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/files/important_file_writer.h>
#include <base/files/scoped_file.h>
#include <base/guid.h>
#include <base/json/json_string_value_serializer.h>
#include <base/logging.h>
#include <base/numerics/safe_conversions.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_util.h>
#include <base/version.h>
#include <brillo/flag_helper.h>
#include <crypto/secure_hash.h>
#include <crypto/sha2.h>
#include <crypto/signature_verifier.h>
#include <dbus-c++/error.h>
#include "imageloader_common.h"
namespace imageloader {
namespace {
using imageloader::kBadResult;
// The name of the fingerprint file.
constexpr char kFingerprintName[] = "manifest.fingerprint";
// The name of the imageloader manifest file.
constexpr char kManifestName[] = "imageloader.json";
// The manifest signature.
constexpr char kManifestSignatureName[] = "imageloader.sig.1";
// The current version of the manifest file.
constexpr int kCurrentManifestVersion = 1;
// The name of the version field in the manifest.
constexpr char kManifestVersionField[] = "manifest-version";
// The name of the component version field in the manifest.
constexpr char kVersionField[] = "version";
// The name of the field containing the image hash.
constexpr char kImageHashField[] = "image-sha256-hash";
// The name of the image file.
constexpr char kImageFileName[] = "image.squash";
// The name of the field containing the table hash.
constexpr char kTableHashField[] = "table-sha256-hash";
// The name of the file containing the latest component version.
constexpr char kLatestVersionFile[] = "latest-version";
// The name of the table file.
constexpr char kTableFileName[] = "table";
// The permissions that the component update directory must use.
constexpr int kComponentDirPerms = 0755;
// The permissions that files in the component should have.
constexpr int kComponentFilePerms = 0644;
// The maximum size of any file to read into memory.
constexpr size_t kMaximumFilesize = 4096 * 10;
// TODO(kerrnel): A component should be abstracted into a class that hides the
// disk structure.
// A component is a directory on disk with a hierarchy of files inside of it. It
// looks like this for an example component, "PepperFlashPlayer":
// PepperFlashPlayer/
// - latest_version # a file containing the latest version, in this case
// # (22.0.0.158)
// - 22.0.0.158/ # the folder containing the actual component.
// - imageloader.json # a manifest file containing the hashes of all the
// # files, and some other metadata.
// - imageloader.sig.1 # a signature blob of the imageloader.json manifest.
// - table # contains the device mapper table (including merkle
// # tree root hash), of the dm-verity image.
// - image.squash # The squashfs disk image containing the actual
// # component files. The dm-verity hash tree is appended
// # to the end of the disk image, and the |table| tells
// # the kernel the offset of the tree.
// Functions to generate paths to various files in the components.
base::FilePath GetComponentPath(const base::FilePath& storage_root,
const std::string& component_name) {
return storage_root.Append(component_name);
}
// |component_root_path| is the "PepperFlashPlayer" folder in the above example.
base::FilePath GetVersionFilePath(const base::FilePath& component_root_path) {
return component_root_path.Append(kLatestVersionFile);
}
base::FilePath GetVersionedPath(const base::FilePath& component_root_path,
const std::string& current_version) {
return component_root_path.Append(current_version);
}
// |component_version_path| must refer to an actual version of the component,
// such as "22.0.0.158" in the above example.
base::FilePath GetManifestPath(const base::FilePath& component_version_path) {
return component_version_path.Append(kManifestName);
}
base::FilePath GetSignaturePath(const base::FilePath& component_version_path) {
return component_version_path.Append(kManifestSignatureName);
}
base::FilePath GetFingerprintPath(
const base::FilePath& component_version_path) {
return component_version_path.Append(kFingerprintName);
}
base::FilePath GetTablePath(const base::FilePath& component_version_path) {
return component_version_path.Append(kTableFileName);
}
base::FilePath GetImagePath(const base::FilePath& component_version_path) {
return component_version_path.Append(kImageFileName);
}
// |mount_base_path| is the subfolder where all components are mounted.
// For example "/mnt/imageloader."
base::FilePath GetMountPoint(const base::FilePath& mount_base_path,
const std::string& component_name,
const std::string& component_version) {
return mount_base_path.Append(component_name).Append(component_version);
}
bool AssertComponentDirPerms(const base::FilePath& path) {
int mode;
if (!GetPosixFilePermissions(path, &mode)) return false;
return mode == kComponentDirPerms;
}
bool GetSHA256FromString(const std::string& hash_str,
std::vector<uint8_t>* bytes) {
if (!base::HexStringToBytes(hash_str, bytes)) return false;
return bytes->size() == crypto::kSHA256Length;
}
bool WriteFileToDisk(const base::FilePath& path, const std::string& contents) {
base::ScopedFD fd(HANDLE_EINTR(open(path.value().c_str(),
O_CREAT | O_WRONLY | O_EXCL,
kComponentFilePerms)));
if (!fd.is_valid()) {
PLOG(ERROR) << "Error creating file for " << path.value();
return false;
}
base::File file(fd.release());
int size = base::checked_cast<int>(contents.size());
return file.Write(0, contents.data(), contents.size()) == size;
}
bool CreateDirectoryWithMode(const base::FilePath& full_path, int mode) {
std::vector<base::FilePath> subpaths;
// Collect a list of all parent directories.
base::FilePath last_path = full_path;
subpaths.push_back(full_path);
for (base::FilePath path = full_path.DirName();
path.value() != last_path.value(); path = path.DirName()) {
subpaths.push_back(path);
last_path = path;
}
// Iterate through the parents and create the missing ones.
for (const auto& subpath : base::Reversed(subpaths)) {
if (base::DirectoryExists(subpath)) continue;
if (mkdir(subpath.value().c_str(), mode) == 0) continue;
// Mkdir failed, but it might have failed with EEXIST, or some other error
// due to the the directory appearing out of thin air. This can occur if
// two processes are trying to create the same file system tree at the same
// time. Check to see if it exists and make sure it is a directory.
if (!base::DirectoryExists(subpath)) {
PLOG(ERROR) << "Failed to create directory";
return false;
}
}
return true;
}
bool GetAndVerifyTable(const base::FilePath& path,
const std::vector<uint8_t>& hash,
std::string* out_table) {
std::string table;
if (!base::ReadFileToStringWithMaxSize(path, &table, kMaximumFilesize)) {
return false;
}
std::vector<uint8_t> table_hash(crypto::kSHA256Length);
crypto::SHA256HashString(table, table_hash.data(), table_hash.size());
if (table_hash != hash) {
LOG(ERROR) << "dm-verity table file has the wrong hash.";
return false;
}
out_table->assign(table);
return true;
}
// |callback| is repeatedly called on data read from the file, can return false on
// error to terminate the read early.
bool ReadFileWithCallback(base::File* file,
std::function<bool(const char*, int)> callback) {
int size = file->GetLength();
if (size <= 0) return false;
int rv = 0, bytes_read = 0;
char buf[4096];
do {
int remaining = size - bytes_read;
int bytes_to_read =
std::min(remaining, base::checked_cast<int>(sizeof(buf)));
rv = file->ReadAtCurrentPos(buf, bytes_to_read);
if (rv <= 0) break;
bytes_read += rv;
if (!callback(buf, rv)) return false;
} while (bytes_read <= size);
return bytes_read == size;
}
bool GetAndVerifyImage(const base::FilePath& path,
const std::vector<uint8_t>& hash,
base::ScopedFD* fd) {
base::File image(path, base::File::FLAG_OPEN | base::File::FLAG_READ);
if (!image.IsValid()) {
LOG(ERROR) << "Could not open image file.";
return false;
}
std::unique_ptr<crypto::SecureHash> sha256(
crypto::SecureHash::Create(crypto::SecureHash::SHA256));
if (!ReadFileWithCallback(&image, [&sha256](const char* buf, int length) {
sha256->Update(buf, length);
return true;
})) {
LOG(ERROR) << "Failed to read image file.";
return false;
}
std::vector<uint8_t> file_hash(crypto::kSHA256Length);
sha256->Finish(file_hash.data(), file_hash.size());
if (hash != file_hash) {
LOG(ERROR) << "Image is corrupt or modified.";
return false;
}
fd->reset(image.TakePlatformFile());
return true;
}
bool CreateMountPointIfNeeded(const base::FilePath& mount_point,
bool* already_mounted) {
*already_mounted = false;
// Is this mount point somehow already taken?
struct stat st;
if (lstat(mount_point.value().c_str(), &st) == 0) {
if (!S_ISDIR(st.st_mode)) {
LOG(ERROR) << "Mount point exists but is not a directory.";
return false;
}
base::FilePath mount_parent = mount_point.DirName();
struct stat st2;
if (stat(mount_parent.value().c_str(), &st2) != 0) {
PLOG(ERROR) << "Could not stat the mount point parent";
return false;
}
if (st.st_dev != st2.st_dev) {
struct statfs st_fs;
if (statfs(mount_point.value().c_str(), &st_fs) != 0) {
PLOG(ERROR) << "statfs";
return false;
}
if (st_fs.f_type != SQUASHFS_MAGIC || !(st_fs.f_flags & ST_NODEV) ||
!(st_fs.f_flags & ST_NOSUID) || !(st_fs.f_flags & ST_RDONLY)) {
LOG(ERROR) << "File system is not the expected type.";
return false;
}
LOG(INFO) << "The mount point already exists: " << mount_point.value();
*already_mounted = true;
return true;
}
} else if (!CreateDirectoryWithMode(mount_point, kComponentDirPerms)) {
LOG(ERROR) << "Failed to create mount point: " << mount_point.value();
return false;
}
return true;
}
} // namespace {}
bool ImageLoaderImpl::GetManifestForComponent(const std::string& name,
Manifest* manifest) {
base::FilePath component_path(GetComponentPath(config_.storage_dir, name));
if (!base::PathExists(component_path)) {
LOG(ERROR) << "No valid component [" << name << "] on disk.";
return false;
}
return GetAndVerifyManifest(name, component_path, manifest, nullptr, nullptr);
}
bool ImageLoaderImpl::GetAndVerifyManifest(const std::string& component_name,
const base::FilePath& component_path,
Manifest* manifest,
std::string* manifest_out,
std::string* sig) {
std::string version;
base::FilePath versioned_path;
if (component_name != "") {
base::FilePath version_hint_path(GetVersionFilePath(component_path));
if (!base::ReadFileToStringWithMaxSize(version_hint_path, &version,
kMaximumFilesize)) {
LOG(ERROR) << "Could not read current version of component ["
<< component_name << "]";
return false;
}
versioned_path = GetVersionedPath(component_path, version);
} else {
// If the component has not been moved into its versioned path yet, there is
// no versioned path, and so the caller passes an empty |component_name|,
// telling this function to work with the |component_path| directly.
versioned_path = component_path;
}
std::string manifest_str;
std::string manifest_sig;
if (!base::ReadFileToStringWithMaxSize(GetManifestPath(versioned_path),
&manifest_str, kMaximumFilesize)) {
LOG(ERROR) << "Could not read manifest file.";
return false;
}
if (!base::ReadFileToStringWithMaxSize(GetSignaturePath(versioned_path),
&manifest_sig, kMaximumFilesize)) {
LOG(ERROR) << "Could not read signature file.";
return false;
}
if (!VerifyAndParseManifest(manifest_str, manifest_sig, manifest)) {
LOG(ERROR) << "Could not verify and parse manifest file.";
return false;
}
if (component_name != "" && manifest->version != version) {
LOG(ERROR) << "Manifest version does not match hint file version.";
return false;
}
if (manifest_out) manifest_out->assign(manifest_str);
if (sig) sig->assign(manifest_sig);
return true;
}
// The client inserts manifest.fingerprint into components after unpacking the
// CRX. The file is used for delta updates. Since Chrome OS doesn't rely on it
// for security of the disk image, we are fine with sanity checking the contents
// and then preserving the unsigned file.
bool ImageLoaderImpl::IsValidFingerprintFile(const std::string& contents) {
return contents.size() <= 256 &&
std::find_if_not(contents.begin(), contents.end(), [](char ch) {
return base::IsAsciiAlpha(ch) || base::IsAsciiDigit(ch) || ch == '.';
}) == contents.end();
}
bool ImageLoaderImpl::CopyFingerprintFile(const base::FilePath& src,
const base::FilePath& dest) {
base::FilePath fingerprint_path(GetFingerprintPath(src));
if (base::PathExists(fingerprint_path)) {
std::string fingerprint_contents;
if (!base::ReadFileToStringWithMaxSize(
fingerprint_path, &fingerprint_contents, kMaximumFilesize)) {
return false;
}
if (!IsValidFingerprintFile(fingerprint_contents)) return false;
if (!WriteFileToDisk(GetFingerprintPath(dest), fingerprint_contents)) {
return false;
}
}
return true;
}
bool ImageLoaderImpl::CopyAndHashFile(
const base::FilePath& src_path, const base::FilePath& dest_path,
const std::vector<uint8_t>& expected_hash) {
base::File file(src_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
if (!file.IsValid()) return false;
base::ScopedFD dest(
HANDLE_EINTR(open(dest_path.value().c_str(), O_CREAT | O_WRONLY | O_EXCL,
kComponentFilePerms)));
if (!dest.is_valid()) return false;
base::File out_file(dest.release());
std::unique_ptr<crypto::SecureHash> sha256(
crypto::SecureHash::Create(crypto::SecureHash::SHA256));
if (!ReadFileWithCallback(
&file, [&sha256, &out_file](const char* buf, int length) {
sha256->Update(buf, length);
return out_file.WriteAtCurrentPos(buf, length) == length;
})) {
LOG(ERROR) << "Failed to read image file.";
return false;
}
std::vector<uint8_t> file_hash(crypto::kSHA256Length);
sha256->Finish(file_hash.data(), file_hash.size());
if (expected_hash != file_hash) {
LOG(ERROR) << "Image is corrupt or modified.";
return false;
}
return true;
}
bool ImageLoaderImpl::VerifyAndParseManifest(
const std::string& manifest_contents, const base::StringPiece signature,
Manifest* manifest) {
// Verify the manifest before trusting any of its contents.
if (!ECVerify(manifest_contents, signature)) {
LOG(INFO) << "Manifest did not pass signature verification.";
return false;
}
// Now deserialize the manifest json and read out the rest of the component.
int error_code;
std::string error_message;
JSONStringValueDeserializer deserializer(manifest_contents);
std::unique_ptr<base::Value> value =
deserializer.Deserialize(&error_code, &error_message);
if (!value) {
LOG(ERROR) << "Could not deserialize the manifest file. Error "
<< error_code << ": " << error_message;
return false;
}
base::DictionaryValue* manifest_dict = nullptr;
if (!value->GetAsDictionary(&manifest_dict)) {
LOG(ERROR) << "Could not parse manifest file as JSON.";
return false;
}
// This will have to be changed if the manifest version is bumped.
int version;
if (!manifest_dict->GetInteger(kManifestVersionField, &version)) {
LOG(ERROR) << "Could not parse manifest version field from manifest.";
return false;
}
if (version != kCurrentManifestVersion) {
LOG(ERROR) << "Unsupported version of the manifest.";
return false;
}
manifest->manifest_version = version;
std::string image_hash_str;
if (!manifest_dict->GetString(kImageHashField, &image_hash_str)) {
LOG(ERROR) << "Could not parse image hash from manifest.";
return false;
}
if (!GetSHA256FromString(image_hash_str, &(manifest->image_sha256))) {
LOG(ERROR) << "Could not convert image hash to bytes.";
return false;
}
std::string table_hash_str;
if (!manifest_dict->GetString(kTableHashField, &table_hash_str)) {
LOG(ERROR) << "Could not parse table hash from manifest.";
return false;
}
if (!GetSHA256FromString(table_hash_str, &(manifest->table_sha256))) {
LOG(ERROR) << "Could not convert table hash to bytes.";
return false;
}
if (!manifest_dict->GetString(kVersionField, &(manifest->version))) {
LOG(ERROR) << "Could not parse component version from manifest.";
return false;
}
return true;
}
bool ImageLoaderImpl::CopyComponentDirectory(
const base::FilePath& component_path,
const base::FilePath& destination_folder, const std::string& version) {
if (mkdir(destination_folder.value().c_str(), kComponentDirPerms) != 0) {
PLOG(ERROR) << "Failed to create directory " << destination_folder.value();
return false;
}
std::string manifest_str;
std::string manifest_sig;
Manifest manifest;
if (!GetAndVerifyManifest("", component_path, &manifest, &manifest_str,
&manifest_sig)) {
return false;
}
if (!WriteFileToDisk(GetManifestPath(destination_folder), manifest_str) ||
!WriteFileToDisk(GetSignaturePath(destination_folder), manifest_sig)) {
LOG(ERROR) << "Could not write manifest and signature to disk.";
return false;
}
if (manifest.version != version) {
LOG(ERROR) << "The client provided a different component version than the "
"manifest.";
return false;
}
base::FilePath table_src(GetTablePath(component_path));
base::FilePath table_dest(GetTablePath(destination_folder));
if (!CopyAndHashFile(table_src, table_dest, manifest.table_sha256)) {
LOG(ERROR) << "Could not copy table file.";
return false;
}
base::FilePath image_src(GetImagePath(component_path));
base::FilePath image_dest(GetImagePath(destination_folder));
if (!CopyAndHashFile(image_src, image_dest, manifest.image_sha256)) {
LOG(ERROR) << "Could not copy image file.";
return false;
}
if (!CopyFingerprintFile(component_path, destination_folder)) {
LOG(ERROR) << "Could not copy manifest.fingerprint file.";
return false;
}
return true;
}
bool ImageLoaderImpl::ECVerify(const base::StringPiece data,
const base::StringPiece sig) {
crypto::SignatureVerifier verifier;
if (!verifier.VerifyInit(crypto::SignatureVerifier::ECDSA_SHA256,
reinterpret_cast<const uint8_t*>(sig.data()),
base::checked_cast<int>(sig.size()),
config_.key.data(),
base::checked_cast<int>(config_.key.size()))) {
return false;
}
verifier.VerifyUpdate(reinterpret_cast<const uint8_t*>(data.data()),
base::checked_cast<int>(data.size()));
return verifier.VerifyFinal();
}
bool ImageLoaderImpl::LoadComponentHelper(const std::string& name,
const Manifest& manifest,
const base::FilePath& mount_point) {
// First check if the component is already mounted and avoid unnecessary work.
bool already_mounted = false;
if (!CreateMountPointIfNeeded(mount_point, &already_mounted)) return false;
if (already_mounted) return true;
base::FilePath component_path(GetComponentPath(config_.storage_dir, name));
if (!base::PathExists(component_path)) {
LOG(ERROR) << "No valid component [" << name << "] on disk.";
return false;
}
base::FilePath versioned_path(
GetVersionedPath(component_path, manifest.version));
// Now read the table in and verify the hash.
std::string table;
if (!GetAndVerifyTable(GetTablePath(versioned_path), manifest.table_sha256,
&table)) {
LOG(ERROR) << "Could not read and verify dm-verity table.";
return false;
}
// Before we mount the image, check the hash as a sanity check.
base::ScopedFD image_fd;
base::FilePath image_path(GetImagePath(versioned_path));
if (!GetAndVerifyImage(image_path, manifest.image_sha256, &image_fd)) {
LOG(ERROR) << "Failed to load and verify the disk image.";
return false;
}
// The mount point is not yet taken, so go ahead.
if (!config_.verity_mounter->Mount(image_fd, mount_point, table)) {
LOG(ERROR) << "Failed to mount image.";
return false;
}
return true;
}
bool ImageLoaderImpl::LoadComponent(const std::string& name,
const std::string& mount_point_str) {
Manifest manifest;
if (!GetManifestForComponent(name, &manifest)) return false;
base::FilePath mount_point(mount_point_str);
return LoadComponentHelper(name, manifest, mount_point);
}
std::string ImageLoaderImpl::LoadComponent(const std::string& name) {
Manifest manifest;
if (!GetManifestForComponent(name, &manifest)) return kBadResult;
base::FilePath mount_point(
GetMountPoint(config_.mount_path, name, manifest.version));
return LoadComponentHelper(name, manifest, mount_point) ? mount_point.value()
: kBadResult;
}
bool ImageLoaderImpl::RegisterComponent(
const std::string& name, const std::string& version,
const std::string& component_folder_abs_path) {
base::FilePath components_dir(config_.storage_dir);
if (!base::PathExists(components_dir)) {
if (mkdir(components_dir.value().c_str(), kComponentDirPerms) != 0) {
PLOG(ERROR) << "Could not create the ImageLoader components directory.";
return false;
}
}
if (!AssertComponentDirPerms(config_.storage_dir)) return false;
base::FilePath component_root(GetComponentPath(components_dir, name));
std::string current_version_hint;
base::FilePath version_hint_path(GetVersionFilePath(component_root));
bool was_previous_version = base::PathExists(version_hint_path);
if (was_previous_version) {
if (!base::ReadFileToStringWithMaxSize(
version_hint_path, &current_version_hint, kMaximumFilesize)) {
return false;
}
// Check for version rollback. We trust the version from the directory name
// because it had to be validated to ever be registered.
base::Version current_version(current_version_hint);
base::Version new_version(version);
if (!current_version.IsValid() || !new_version.IsValid()) {
return false;
}
if (new_version <= current_version) {
LOG(ERROR) << "Version [" << new_version << "] is not newer than ["
<< current_version << "] for component [" << name
<< "] and cannot be registered.";
return false;
}
}
// Check if this specific component already exists in the filesystem.
if (!base::PathExists(component_root)) {
if (mkdir(component_root.value().c_str(), kComponentDirPerms) != 0) {
PLOG(ERROR) << "Could not create component specific directory.";
return false;
}
}
// Take ownership of the component and verify it.
base::FilePath version_path(GetVersionedPath(component_root, version));
base::FilePath folder_path(component_folder_abs_path);
// If |version_path| exists but was not the active version, ImageLoader
// probably crashed previously and could not cleanup.
if (base::PathExists(version_path)) {
base::DeleteFile(version_path, /*recursive=*/true);
}
if (!CopyComponentDirectory(folder_path, version_path, version)) {
base::DeleteFile(version_path, /*recursive=*/true);
return false;
}
if (!base::ImportantFileWriter::WriteFileAtomically(version_hint_path,
version)) {
base::DeleteFile(version_path, /*recursive=*/true);
LOG(ERROR) << "Failed to update current version hint file.";
return false;
}
// Now delete the old component version, if there was one.
if (was_previous_version) {
base::DeleteFile(GetVersionedPath(component_root, current_version_hint),
true);
}
return true;
}
std::string ImageLoaderImpl::GetComponentVersion(const std::string& name) {
base::FilePath component_path(GetComponentPath(config_.storage_dir, name));
if (!base::PathExists(component_path)) {
LOG(ERROR) << "No valid component [" << name << "] on disk.";
return kBadResult;
}
Manifest manifest;
if (!GetAndVerifyManifest(name, component_path, &manifest, nullptr, nullptr))
return kBadResult;
return manifest.version;
}
} // namespace imageloader