diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | utility/Makefile | 18 | ||||
-rw-r--r-- | utility/mount-encrypted.c | 815 | ||||
-rw-r--r-- | utility/mount-encrypted.h | 99 | ||||
-rw-r--r-- | utility/mount-helpers.c | 650 | ||||
-rw-r--r-- | utility/mount-helpers.h | 40 |
6 files changed, 1624 insertions, 0 deletions
@@ -1,2 +1,4 @@ /build +/build-au +/build-main ID diff --git a/utility/Makefile b/utility/Makefile index e0a0f221..59d624a5 100644 --- a/utility/Makefile +++ b/utility/Makefile @@ -40,6 +40,8 @@ TARGET_NAMES = crossystem \ ifeq ($(MINIMAL),) TARGET_NAMES += bmpblk_font bmpblk_utility eficompress efidecompress +else +TARGET_NAMES += mount-encrypted endif TARGET_BINS = $(addprefix ${BUILD_ROOT}/,$(TARGET_NAMES)) @@ -157,6 +159,22 @@ ${BUILD_ROOT}/vbutil_what_keys: vbutil_what_keys ${BUILD_ROOT}/tpmc: tpmc.c $(LIBS) $(CC) $(CFLAGS) $< -o $@ $(LIBS) +${BUILD_ROOT}/mount-helpers.o: mount-helpers.c mount-helpers.h mount-encrypted.h + $(CC) -Wall -Werror -O2 -D_FORTIFY_SOURCE=2 -fstack-protector \ + $(shell pkg-config --cflags glib-2.0 openssl) \ + -c $< -o $@ + +# The embedded libcrypto includes conflict with the shipped openssl, +# so this builds without the common CFLAGS (and those includes). +${BUILD_ROOT}/mount-encrypted: mount-encrypted.c mount-encrypted.h \ + ${BUILD_ROOT}/mount-helpers.o $(LIBS) + $(CC) -Wall -Werror -O2 -D_FORTIFY_SOURCE=2 -fstack-protector \ + $(shell pkg-config --cflags glib-2.0 openssl) \ + -I$(FWDIR)/include \ + -I$(HOSTDIR)/include \ + $< -o $@ $(shell pkg-config --libs glib-2.0 openssl) \ + ${BUILD_ROOT}/mount-helpers.o $(LIBS) + ${BUILD_ROOT}/dev_sign_file: dev_sign_file.c $(LIBS) $(CC) $(CFLAGS) $< -o $@ $(LIBS) -lcrypto diff --git a/utility/mount-encrypted.c b/utility/mount-encrypted.c new file mode 100644 index 00000000..2a1a49d5 --- /dev/null +++ b/utility/mount-encrypted.c @@ -0,0 +1,815 @@ +/* Copyright (c) 2012 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. + * + * This tool will attempt to mount or create the encrypted stateful partition, + * and the various bind mountable subdirectories. + * + */ +#define _GNU_SOURCE +#define _FILE_OFFSET_BITS 64 +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <fcntl.h> +#include <grp.h> +#include <pwd.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/statvfs.h> +#include <sys/time.h> +#include <sys/types.h> +#include <sys/mount.h> +#include <linux/fs.h> + +#include <glib.h> + +#include <openssl/rand.h> + +#define CHROMEOS_ENVIRONMENT +#include "tlcl.h" +#include "crossystem.h" + +#include "mount-encrypted.h" +#include "mount-helpers.h" + +#define STATEFUL_MNT "/mnt/stateful_partition" +#define ENCRYPTED_MNT STATEFUL_MNT "/encrypted" +#define DMCRYPT_DEV_NAME "encstateful" +#define BUF_SIZE 1024 +#define PROP_SIZE 64 + +static const gchar * const kRootDir = "/"; +static const gchar * const kKernelCmdline = "/proc/cmdline"; +static const gchar * const kKernelCmdlineOption = " encrypted-stateful-key="; +static const gchar * const kStatefulMount = STATEFUL_MNT; +static const gchar * const kEncryptedKey = STATEFUL_MNT "/encrypted.key"; +static const gchar * const kEncryptedBlock = STATEFUL_MNT "/encrypted.block"; +static const gchar * const kEncryptedMount = ENCRYPTED_MNT; +static const gchar * const kEncryptedFSType = "ext4"; +static const gchar * const kCryptName = DMCRYPT_DEV_NAME; +static const gchar * const kCryptDev = "/dev/mapper/" DMCRYPT_DEV_NAME; +static const gchar * const kTpmDev = "/dev/tpm0"; +static const gchar * const kNullDev = "/dev/null"; +static const float kSizePercent = 0.3; +static const uint32_t kLockboxIndex = 0x20000004; +static const uint32_t kLockboxSizeV1 = 0x2c; +static const uint32_t kLockboxSizeV2 = 0x45; +static const uint32_t kLockboxSaltOffset = 0x5; +static const size_t kSectorSize = 512; +static const size_t kExt4BlockSize = 4096; +static const size_t kExt4MinBytes = 64 * 1024 * 1024; + +static struct bind_mount { + const char * const src; + const char * const old; + const char * const dst; + const char * const owner; + const char * const group; + const mode_t mode; + const int submount; /* Submount is bound already. */ + const int optional; /* Non-fatal if this bind fails. */ +} bind_mounts[] = { +#if DEBUG_ENABLED == 2 +# define DEBUG_DEST ".new" +#else +# define DEBUG_DEST "" +#endif + { ENCRYPTED_MNT "/var", STATEFUL_MNT "/var", + "/var" DEBUG_DEST, "root", "root", + S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH, 0, 0 }, + { ENCRYPTED_MNT "/chronos", STATEFUL_MNT "/home/chronos", + "/home/chronos" DEBUG_DEST, "chronos", "chronos", + S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH, 1, 1 }, + { }, +}; + +int has_tpm = 0; + +static void sha256(char *string, uint8_t *digest) +{ + SHA256((unsigned char *)string, strlen(string), digest); +} + +/* Extract the desired system key from the kernel's boot command line. */ +static int get_key_from_cmdline(uint8_t *digest) +{ + int result = 0; + gchar *buffer; + gsize length; + char *cmdline, *option_end; + /* Option name without the leading space. */ + const gchar *option = kKernelCmdlineOption + 1; + + if (!g_file_get_contents(kKernelCmdline, &buffer, &length, NULL)) { + PERROR(kKernelCmdline); + return 0; + } + + /* Find a string match either at start of string or following + * a space. + */ + cmdline = buffer; + if (strncmp(cmdline, option, strlen(option)) == 0 || + (cmdline = strstr(cmdline, kKernelCmdlineOption))) { + /* The "=" exists because it is in kKernelCmdlineOption. */ + cmdline = strstr(cmdline, "="); + /* strchrnul() cannot return NULL. */ + option_end = strchrnul(cmdline, ' '); + *option_end = '\0'; + sha256(cmdline, digest); + debug_dump_hex("system key", digest, DIGEST_LENGTH); + result = 1; + } + + g_free(buffer); + return result; +} + +static int has_chromefw(void) +{ + static int state = -1; + char fw[PROP_SIZE]; + + /* Cache the state so we don't have to perform the query again. */ + if (state != -1) + return state; + + if (!VbGetSystemPropertyString("mainfw_type", fw, sizeof(fw))) + state = 0; + else + state = strcmp(fw, "nonchrome") != 0; + return state; +} + +static int is_cr48(void) +{ + static int state = -1; + char hwid[PROP_SIZE]; + + /* Cache the state so we don't have to perform the query again. */ + if (state != -1) + return state; + + if (!VbGetSystemPropertyString("hwid", hwid, sizeof(hwid))) + state = 0; + else + state = strstr(hwid, "MARIO") != NULL; + return state; +} + +static int +_read_nvram(uint8_t *buffer, size_t len, uint32_t index, uint32_t size) +{ + if (size > len) { + ERROR("NVRAM size (0x%x > 0x%zx) is too big", size, len); + return 0; + } + + return TlclRead(index, buffer, size); +} + +/* + * Cases: + * - no NVRAM area at all (OOBE) + * - defined NVRAM area, but TPM not Owned + * - defined NVRAM area, but not Finalized + * - legacy NVRAM area (migration needed) + * - modern NVRAM area (\o/) + */ +// TODO(keescook): recovery code needs to wipe NVRAM area to new size? +static int get_nvram_key(uint8_t *digest, int *old_lockbox) +{ + TPM_PERMANENT_FLAGS pflags; + uint8_t value[kLockboxSizeV2], bytes_anded, bytes_ored; + uint32_t size, result, i; + uint8_t *rand_bytes; + uint32_t rand_size; + + /* Start by expecting modern NVRAM area. */ + *old_lockbox = 0; + size = kLockboxSizeV2; + result = _read_nvram(value, sizeof(value), kLockboxIndex, size); + if (result) { + size = kLockboxSizeV1; + result = _read_nvram(value, sizeof(value), kLockboxIndex, size); + if (result) { + /* No NVRAM area at all. */ + INFO("No NVRAM area defined."); + return 0; + } + /* Legacy NVRAM area. */ + INFO("Legacy NVRAM area found."); + *old_lockbox = 1; + } else { + INFO("NVRAM area found."); + } + + debug_dump_hex("nvram", value, size); + + /* Ignore defined but unowned NVRAM area. */ + result = TlclGetPermanentFlags(&pflags); + if (result) { + INFO("Could not read TPM Permanent Flags."); + return 0; + } + if (!pflags.ownership) { + INFO("TPM not Owned, ignoring NVRAM area."); + return 0; + } + + /* Ignore defined but unwritten NVRAM area. */ + bytes_ored = 0x0; + bytes_anded = 0xff; + for (i = 0; i < size; ++i) { + bytes_ored |= value[i]; + bytes_anded &= value[i]; + } + if (bytes_ored == 0x0 || bytes_anded == 0xff) { + INFO("NVRAM area has been defined but not written."); + return 0; + } + + /* Choose random bytes to use based on NVRAM version. */ + if (*old_lockbox) { + rand_bytes = value; + rand_size = size; + } else { + rand_bytes = value + kLockboxSaltOffset; + if (kLockboxSaltOffset + DIGEST_LENGTH > size) { + INFO("Impossibly small NVRAM area size (%d).", size); + return 0; + } + rand_size = DIGEST_LENGTH; + } + if (rand_size < DIGEST_LENGTH) { + INFO("Impossibly small rand_size (%d).", rand_size); + return 0; + } + debug_dump_hex("rand_bytes", rand_bytes, rand_size); + + SHA256(rand_bytes, rand_size, digest); + debug_dump_hex("system key", digest, DIGEST_LENGTH); + + return 1; +} + +/* Find the system key used for decrypting the stored encryption key. + * ChromeOS devices are required to use the NVRAM area (excepting CR-48s), + * all the rest will fallback through various places (kernel command line, + * BIOS UUID, and finally a static value) for a system key. + */ +static int find_system_key(uint8_t *digest, int *migration_allowed) +{ + gchar *key; + gsize length; + + /* By default, do not allow migration. */ + *migration_allowed = 0; + if (has_chromefw()) { + int rc; + rc = get_nvram_key(digest, migration_allowed); + + /* Since the CR-48 did not ship with a lockbox area, they + * are allowed to fall back to non-NVRAM system keys. + */ + if (rc || !is_cr48()) { + INFO("Using NVRAM as system key; %s.", + rc ? "already populated" + : "needs population"); + return rc; + } + } + + if (get_key_from_cmdline(digest)) { + INFO("Using kernel command line argument as system key."); + return 1; + } + if (g_file_get_contents("/sys/class/dmi/id/product_uuid", + &key, &length, NULL)) { + sha256(key, digest); + debug_dump_hex("system key", digest, DIGEST_LENGTH); + g_free(key); + INFO("Using UUID as system key."); + return 1; + } + + INFO("Using default insecure system key."); + sha256("default unsafe static key", digest); + debug_dump_hex("system key", digest, DIGEST_LENGTH); + return 1; +} + +/* Returns 1 on success, 0 on failure. */ +static int get_random_bytes_tpm(unsigned char *buffer, int wanted) +{ + uint32_t remaining = wanted; + + /* Read random bytes from TPM, which can return short reads. */ + while (remaining) { + uint32_t result, size; + + result = TlclGetRandom(buffer + (wanted - remaining), + remaining, &size); + if (result || size > remaining) { + ERROR("TPM GetRandom failed."); + return 0; + } + remaining -= size; + } + + return 1; +} + +/* Returns 1 on success, 0 on failure. */ +static int get_random_bytes(unsigned char *buffer, int wanted) +{ + if (has_tpm) + return get_random_bytes_tpm(buffer, wanted); + else + return RAND_bytes(buffer, wanted); +} + +static char *choose_encryption_key(void) +{ + unsigned char rand_bytes[DIGEST_LENGTH]; + unsigned char digest[DIGEST_LENGTH]; + + get_random_bytes(rand_bytes, sizeof(rand_bytes)); + + SHA256(rand_bytes, DIGEST_LENGTH, digest); + debug_dump_hex("encryption key", digest, DIGEST_LENGTH); + + return stringify_hex(digest, DIGEST_LENGTH); +} + +static int check_bind_src(struct bind_mount *bind) +{ + struct passwd *user; + struct group *group; + + if (!(user = getpwnam(bind->owner))) { + PERROR("getpwnam(%s)", bind->owner); + return -1; + } + if (!(group = getgrnam(bind->group))) { + PERROR("getgrnam(%s)", bind->group); + return -1; + } + + if (access(bind->src, R_OK) && mkdir(bind->src, bind->mode)) { + PERROR("mkdir(%s)", bind->src); + return -1; + } + /* Must do explicit chmod since mkdir()'s mode respects umask. */ + if (chmod(bind->src, bind->mode)) { + PERROR("chmod(%s)", bind->src); + return -1; + } + if (chown(bind->src, user->pw_uid, group->gr_gid)) { + PERROR("chown(%s)", bind->src); + return -1; + } + + return 0; +} + +static void migrate_contents(struct bind_mount *bind) +{ + gchar *old; + + /* Skip migration if the old bind src is missing. */ + if (!bind->old || access(bind->old, R_OK)) + return; + + INFO("Migrating bind mount contents %s to %s.", bind->old, bind->src); + check_bind_src(bind); + + if (!(old = g_strdup_printf("%s/.", bind->old))) { + PERROR("g_strdup_printf"); + goto remove; + } + + const gchar *cp[] = { + "/bin/cp", "-a", + old, + bind->src, + NULL + }; + + if (runcmd(cp, NULL) != 0) { + /* If the copy failed, it may have partially populated the + * new source, so we need to remove the new source and + * rebuild it. Regardless, the old source must be removed + * as well. + */ + INFO("Failed to migrate %s to %s!", bind->old, bind->src); + remove_tree(bind->src); + check_bind_src(bind); + } + +remove: + g_free(old); + + /* The removal of the old directory needs to happen at finalize + * time, otherwise /var state gets lost on a migration if the + * system is powered off before the encryption key is saved. + */ + return; +} + +static void finalize(uint8_t *system_key, char *encryption_key) +{ + struct bind_mount *bind; + + INFO("Writing keyfile %s.", kEncryptedKey); + if (!keyfile_write(kEncryptedKey, system_key, encryption_key)) { + ERROR("Failed to write %s -- aborting.", kEncryptedKey); + return; + } + + for (bind = bind_mounts; bind->src; ++ bind) { + if (access(bind->old, R_OK)) + continue; + INFO("Removing %s.", bind->old); +#if DEBUG_ENABLED + continue; +#endif + remove_tree(bind->old); + } +} + +/* This triggers the live encryption key to be written to disk, encrypted + * by the system key. It is intended to be called by Cryptohome once the + * TPM is done being set up. If the system key is passed as an argument, + * use it, otherwise attempt to query the TPM again. + */ +static int finalize_from_cmdline(char *key) +{ + uint8_t system_key[DIGEST_LENGTH]; + char *encryption_key; + int migrate; + + if (key) { + if (strlen(key) != 2 * DIGEST_LENGTH) { + ERROR("Invalid key length."); + return EXIT_FAILURE; + } + + if (!hexify_string(key, system_key, DIGEST_LENGTH)) { + ERROR("Failed to convert hex string to byte array"); + return EXIT_FAILURE; + } + } else { + if (!find_system_key(system_key, &migrate)) { + ERROR("Could not locate system key."); + return EXIT_FAILURE; + } + } + + encryption_key = dm_get_key(kCryptDev); + if (!encryption_key) { + ERROR("Could not locate encryption key for %s.", kCryptDev); + return EXIT_FAILURE; + } + + finalize(system_key, encryption_key); + + return EXIT_SUCCESS; +} + +static int setup_encrypted(void) +{ + int has_system_key; + uint8_t system_key[DIGEST_LENGTH]; + char *encryption_key = NULL; + int migrate_allowed = 0, migrate_needed = 0, rebuild = 0; + gchar *lodev = NULL; + size_t sectors; + struct bind_mount *bind; + int sparsefd; + size_t blocks_min, blocks_max; + + /* Use the "system key" to decrypt the "encryption key" stored in + * the stateful partition. + */ + has_system_key = find_system_key(system_key, &migrate_allowed); + if (has_system_key) { + encryption_key = keyfile_read(kEncryptedKey, system_key); + } else { + INFO("No usable system key found."); + } + + if (encryption_key) { + /* If we found a stored encryption key, we've already + * finished a complete login and Cryptohome Finalize + * so migration is finished. + */ + migrate_allowed = 0; + } else { + INFO("Generating new encryption key."); + encryption_key = choose_encryption_key(); + if (!encryption_key) + return 0; + rebuild = 1; + } + + if (rebuild) { + struct statvfs buf; + off_t size; + + /* Wipe out the old files, and ignore errors. */ + unlink(kEncryptedKey); + unlink(kEncryptedBlock); + + /* Calculate the desired size of the new partition. */ + if (statvfs(kStatefulMount, &buf)) { + PERROR(kStatefulMount); + return 0; + } + size = buf.f_blocks; + size *= kSizePercent; + size *= buf.f_frsize; + + INFO("Creating sparse backing file with size %llu.", + (unsigned long long)size); + + /* Create the sparse file. */ + sparsefd = sparse_create(kEncryptedBlock, size); + if (sparsefd < 0) { + PERROR(kEncryptedBlock); + return 0; + } + } else { + sparsefd = open(kEncryptedBlock, O_RDWR | O_NOFOLLOW); + if (sparsefd < 0) { + PERROR(kEncryptedBlock); + return 0; + } + } + + /* Set up loopback device. */ + INFO("Loopback attaching %s.", kEncryptedBlock); + lodev = loop_attach(sparsefd, kEncryptedBlock); + if (!lodev || strlen(lodev) == 0) { + ERROR("loop_attach failed"); + goto failed; + } + + /* Get size as seen by block device. */ + sectors = get_sectors(lodev); + if (!sectors) { + ERROR("Failed to read device size"); + goto lo_cleanup; + } + + /* Mount loopback device with dm-crypt using the encryption key. */ + INFO("Setting up dm-crypt %s as %s.", lodev, kCryptDev); + if (!dm_setup(sectors, encryption_key, kCryptName, lodev, + kCryptDev)) { + ERROR("dm_setup failed"); + goto lo_cleanup; + } + + /* Decide now if any migration will happen. If so, we will not + * grow the new filesystem in the background, since we need to + * copy the contents over before /var is valid again. + */ + if (!rebuild) + migrate_allowed = 0; + if (migrate_allowed) { + for (bind = bind_mounts; bind->src; ++ bind) { + /* Skip mounts that have no prior location defined. */ + if (!bind->old) + continue; + /* Skip mounts that have no prior data on disk. */ + if (access(bind->old, R_OK) != 0) + continue; + + migrate_needed = 1; + } + } + + /* Calculate filesystem min/max size. */ + blocks_max = sectors / (kExt4BlockSize / kSectorSize); + blocks_min = migrate_needed ? blocks_max : + kExt4MinBytes / kExt4BlockSize; + if (rebuild) { + INFO("Building filesystem on %s " + "(blocksize:%zu, min:%zu, max:%zu).", + kCryptDev, kExt4BlockSize, blocks_min, blocks_max); + if (!filesystem_build(kCryptDev, kExt4BlockSize, + blocks_min, blocks_max)) + goto dm_cleanup; + } + + /* Mount the dm-crypt partition finally. */ + INFO("Mounting %s onto %s.", kCryptDev, kEncryptedMount); + if (access(kEncryptedMount, R_OK) && + mkdir(kEncryptedMount, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)) { + PERROR(kCryptDev); + goto dm_cleanup; + } + if (mount(kCryptDev, kEncryptedMount, kEncryptedFSType, + MS_NODEV | MS_NOEXEC | MS_NOSUID | MS_RELATIME, + "discard")) { + PERROR("mount(%s,%s)", kCryptDev, kEncryptedMount); + goto dm_cleanup; + } + + /* Always spawn filesystem resizer, in case growth was interrupted. */ + /* TODO(keescook): if already full size, don't resize. */ + filesystem_resizer(kCryptDev, blocks_min, blocks_max); + + /* If the legacy lockbox NVRAM area exists, we've rebuilt the + * filesystem, and there are old bind sources on disk, attempt + * migration. + */ + if (migrate_needed && migrate_allowed) { + /* Migration needs to happen before bind mounting because + * some partitions were not already on the stateful partition, + * and would be over-mounted by the new bind mount. + */ + for (bind = bind_mounts; bind->src; ++ bind) + migrate_contents(bind); + } + + /* Perform bind mounts. */ + for (bind = bind_mounts; bind->src; ++ bind) { + INFO("Bind mounting %s onto %s.", bind->src, bind->dst); + if (check_bind_src(bind) || + mount(bind->src, bind->dst, "none", MS_BIND, NULL)) { + PERROR("mount(%s,%s)", bind->src, bind->dst); + if (bind->optional) + continue; + goto unbind; + } + } + + /* Devices that are not using NVRAM for their system key do not + * need to wait for the NVRAM area to be populated by Cryptohome + * and a call to "finalize". Devices that already have the NVRAM + * area populated and are being rebuilt don't need to wait for + * Cryptohome because the NVRAM area isn't going to change. + */ + if (rebuild && has_system_key) + finalize(system_key, encryption_key); + + free(lodev); + return 1; + +unbind: + for (bind = bind_mounts; bind->src; ++ bind) { + INFO("Unmounting %s.", bind->dst); + umount(bind->dst); + } + + INFO("Unmounting %s.", kEncryptedMount); + umount(kEncryptedMount); + +dm_cleanup: + INFO("Removing %s.", kCryptDev); + /* TODO(keescook): something holds this open briefly on mkfs failure + * and I haven't been able to catch it yet. Adding an "fuser" call + * here is sufficient to lose the race. Instead, just sleep during + * the error path. + */ + sleep(1); + dm_teardown(kCryptDev); + +lo_cleanup: + INFO("Unlooping %s.", lodev); + loop_detach(lodev); + +failed: + free(lodev); + + return 0; +} + + +static void check_mount_states(void) +{ + struct bind_mount *bind; + + /* Verify stateful partition exists and is mounted. */ + if (access(kStatefulMount, R_OK) || + same_vfs(kStatefulMount, kRootDir)) { + INFO("%s is not mounted.", kStatefulMount); + exit(1); + } + + /* Verify encrypted partition is missing or not already mounted. */ + if (access(kEncryptedMount, R_OK) == 0 && + !same_vfs(kEncryptedMount, kStatefulMount)) { + INFO("%s already appears to be mounted.", kEncryptedMount); + exit(0); + } + + /* Verify that bind mount targets exist. */ + for (bind = bind_mounts; bind->src; ++ bind) { + if (access(bind->dst, R_OK)) { + PERROR("%s mount point is missing.", bind->dst); + exit(1); + } + } + + /* Verify that old bind mounts on stateful haven't happened yet. */ + for (bind = bind_mounts; bind->src; ++ bind) { + if (bind->submount) + continue; + + if (same_vfs(bind->dst, kStatefulMount)) { + INFO("%s already bind mounted.", bind->dst); + exit(1); + } + } + + INFO("VFS mount state sanity check ok."); +} + +int device_details(void) +{ + uint8_t system_key[DIGEST_LENGTH]; + TPM_PERMANENT_FLAGS pflags; + int old_lockbox = -1; + + printf("TPM: %s\n", has_tpm ? "yes" : "no"); + if (has_tpm) { + printf("TPM Owned: %s\n", TlclGetPermanentFlags(&pflags) ? + "fail" : (pflags.ownership ? "yes" : "no")); + } + printf("ChromeOS: %s\n", has_chromefw() ? "yes" : "no"); + printf("CR48: %s\n", is_cr48() ? "yes" : "no"); + if (has_chromefw()) { + int rc; + rc = get_nvram_key(system_key, &old_lockbox); + if (!rc) + printf("NVRAM: missing\n"); + else { + printf("NVRAM: %s, %s\n", + old_lockbox ? "legacy" : "modern", + rc ? "available" : "ignored"); + } + } + else { + printf("NVRAM: not present\n"); + } + + return EXIT_SUCCESS; +} + +void init_tpm(void) +{ + int tpm; + + tpm = open(kTpmDev, O_RDWR); + if (tpm >= 0) { + has_tpm = 1; + close(tpm); + } + else { + /* TlclLibInit does not fail, it exits, so instead, + * have it open /dev/null if the TPM is not available. + */ + setenv("TPM_DEVICE_PATH", kNullDev, 1); + } + TlclLibInit(); +} + +int main(int argc, char *argv[]) +{ + int okay; + + INFO_INIT("Starting."); + init_tpm(); + + if (argc > 1) { + if (!strcmp(argv[1], "device")) + return device_details(); + if (!strcmp(argv[1], "finalize")) + return finalize_from_cmdline(argc > 2 ? argv[2] : NULL); + + fprintf(stderr, "Usage: %s [device|finalize]\n", + argv[0]); + return 1; + } + + check_mount_states(); + + okay = setup_encrypted(); + if (!okay) { + INFO("Setup failed -- clearing files and retrying."); + unlink(kEncryptedKey); + unlink(kEncryptedBlock); + okay = setup_encrypted(); + } + + INFO("Done."); + + /* Continue boot. */ + return !okay; +} diff --git a/utility/mount-encrypted.h b/utility/mount-encrypted.h new file mode 100644 index 00000000..df5b1d2d --- /dev/null +++ b/utility/mount-encrypted.h @@ -0,0 +1,99 @@ +/* Copyright (c) 2012 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. + * + * Private header file for mount-encrypted helper tool. + */ +#ifndef _MOUNT_ENCRYPTED_H_ +#define _MOUNT_ENCRYPTED_H_ + +/* TODO(keescook): Disable debugging in production. */ +#define DEBUG_ENABLED 1 + +#include <openssl/err.h> +#include <openssl/sha.h> + +#define DIGEST_LENGTH SHA256_DIGEST_LENGTH + +#define _ERROR(f, a...) do { \ + fprintf(stderr, "ERROR %s (%s, %d): ", \ + __func__, __FILE__, __LINE__); \ + fprintf(stderr, f, ## a); \ +} while (0) +#define ERROR(f, a...) do { \ + _ERROR(f, ## a); \ + fprintf(stderr, "\n"); \ +} while (0) +#define PERROR(f, a...) do { \ + _ERROR(f, ## a); \ + fprintf(stderr, ": %s\n", strerror(errno)); \ +} while (0) + +#define SSL_ERROR(f, a...) do { \ + ERR_load_crypto_strings(); \ + _ERROR(f, ## a); \ + fprintf(stderr, "%s\n", ERR_error_string(ERR_get_error(), NULL)); \ +} while (0) + +#if DEBUG_ENABLED +static struct timeval tick; +# define TICK_INIT() gettimeofday(&tick, NULL) +# ifdef DEBUG_TIME_DELTA +# define TICK_REPORT() do { \ + struct timeval now, diff; \ + gettimeofday(&now, NULL); \ + diff.tv_sec = now.tv_sec - tick.tv_sec; \ + if (tick.tv_usec > now.tv_usec) { \ + diff.tv_sec -= 1; \ + diff.tv_usec = 1000000 - tick.tv_usec + now.tv_usec; \ + } else { \ + diff.tv_usec = now.tv_usec - tick.tv_usec; \ + } \ + tick = now; \ + printf("\tTook: [%2d.%06d]\n", (int)diff.tv_sec, (int)diff.tv_usec); \ +} while (0) +# else +# define TICK_REPORT() do { \ + gettimeofday(&tick, NULL); \ + printf("[%d:%2d.%06d] ", getpid(), (int)tick.tv_sec, (int)tick.tv_usec); \ +} while (0) +# endif +#else +# define TICK_INIT() do { } while (0) +# define TICK_REPORT() do { } while (0) +#endif + +#define INFO(f, a...) do { \ + TICK_REPORT(); \ + printf(f, ## a); \ + printf("\n"); \ +} while (0) +#define INFO_INIT(f, a...) do { \ + TICK_INIT(); \ + INFO(f, ## a); \ +} while (0) +#if DEBUG_ENABLED +# define DEBUG(f, a...) do { \ + printf(f, ## a); \ + printf("\n"); \ +} while (0) +#else +# define DEBUG(f, a...) do { } while (0) +#endif + +#if DEBUG_ENABLED +static inline void debug_dump_hex(const char *name, uint8_t *data, + uint32_t size) +{ + int i; + printf("%s: ", name); + for (i = 0; i < size; i++) { + printf("%02x ", data[i]); + } + printf("\n"); +} +#else +# define debug_dump_hex(n, d, s) do { } while (0) +#endif + +#endif /* _MOUNT_ENCRYPTED_H_ */ diff --git a/utility/mount-helpers.c b/utility/mount-helpers.c new file mode 100644 index 00000000..b6f31c30 --- /dev/null +++ b/utility/mount-helpers.c @@ -0,0 +1,650 @@ +/* Copyright (c) 2012 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. + * + * This is a collection of helper utilities for use with the "mount-encrypted" + * utility. + * + */ +#define _GNU_SOURCE +#define _FILE_OFFSET_BITS 64 +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <fcntl.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/statvfs.h> +#include <sys/time.h> +#include <sys/types.h> +#include <sys/mount.h> +#include <linux/fs.h> +#include <linux/loop.h> + +#include <glib.h> +#include <glib/gstdio.h> + +#include <openssl/evp.h> + +#include "mount-encrypted.h" +#include "mount-helpers.h" + +static const gchar * const kRootDir = "/"; +static const gchar * const kLoopTemplate = "/dev/loop%d"; +static const int kLoopMajor = 7; +static const int kLoopMax = 8; +static const unsigned int kResizeStepSeconds = 2; +static const size_t kResizeBlocks = 32768 * 10; +static const gchar * const kExt4ExtendedOptions = "discard,lazy_itable_init"; + +int remove_tree(const char *tree) +{ + const gchar *rm[] = { + "/bin/rm", "-rf", tree, + NULL + }; + + return runcmd(rm, NULL); +} + +size_t get_sectors(const char *device) +{ + size_t sectors; + int fd; + if ((fd = open(device, O_RDONLY | O_NOFOLLOW)) < 0) { + PERROR("open(%s)", device); + return 0; + } + if (ioctl(fd, BLKGETSIZE, §ors)) { + PERROR("ioctl(%s, BLKGETSIZE)", device); + return 0; + } + close(fd); + return sectors; +} + +int runcmd(const gchar *argv[], gchar **output) +{ + gint rc; + gchar *out = NULL, *errout = NULL; + GError *err = NULL; + + g_spawn_sync(kRootDir, (gchar **)argv, NULL, 0, NULL, NULL, + &out, &errout, &rc, &err); + if (err) { + ERROR("%s: %s", argv[0], err->message); + g_error_free(err); + return -1; + } + + if (rc) + ERROR("%s failed (%d)\n%s\n%s", argv[0], rc, out, errout); + + if (output) + *output = out; + else + g_free(out); + g_free(errout); + + return rc; +} + +int same_vfs(const char *mnt_a, const char *mnt_b) +{ + struct statvfs stat_a, stat_b; + + if (statvfs(mnt_a, &stat_a)) { + PERROR("statvfs(%s)", mnt_a); + exit(1); + } + if (statvfs(mnt_b, &stat_b)) { + PERROR("statvfs(%s)", mnt_b); + exit(1); + } + return (stat_a.f_fsid == stat_b.f_fsid); +} + +/* Returns allocated string that holds [length]*2 + 1 characters. */ +char *stringify_hex(uint8_t *binary, size_t length) +{ + char *string; + size_t i; + + string = malloc(length * 2 + 1); + if (!string) { + PERROR("malloc"); + return NULL; + } + for (i = 0; i < length; ++i) + sprintf(string + (i * 2), "%02x", binary[i]); + string[length * 2] = '\0'; + + return string; +} + +/* Returns allocated byte array that holds strlen([string])/2 bytes. */ +uint8_t *hexify_string(char *string, uint8_t *binary, size_t length) +{ + size_t bytes, i; + + bytes = strlen(string) / 2; + if (bytes > length) { + ERROR("Hex string too long (%zu) for byte array (%zu)", + bytes, length); + return NULL; + } + + for (i = 0; i < bytes; ++i) { + if (sscanf(&string[i * 2], "%2hhx", &binary[i]) != 1) { + ERROR("Invalid hex code at byte %zu.", i); + return NULL; + } + } + + return binary; +} + +static int is_loop_device(int fd) +{ + struct stat info; + + return (fstat(fd, &info) == 0 && S_ISBLK(info.st_mode) && + major(info.st_rdev) == kLoopMajor); +} + +static int loop_is_attached(int fd) +{ + struct loop_info info; + + errno = 0; + if (ioctl(fd, LOOP_GET_STATUS, &info) && errno == ENXIO) + return 0; + + return 1; +} + +static int loop_allocate(gchar **loopback) +{ + int i, fd; + + *loopback = NULL; + for (i = 0; i < kLoopMax; ++i) { + g_free(*loopback); + *loopback = g_strdup_printf(kLoopTemplate, i); + if (!*loopback) { + PERROR("g_strdup_printf"); + return -1; + } + + fd = open(*loopback, O_RDONLY | O_NOFOLLOW); + if (fd < 0) { + PERROR("open(%s)", *loopback); + goto failed; + } + if (is_loop_device(fd) && !loop_is_attached(fd)) { + close(fd); + fd = open(*loopback, O_RDWR | O_NOFOLLOW); + if (is_loop_device(fd) && !loop_is_attached(fd)) + return fd; + } + close(fd); + } + ERROR("Ran out of loopback devices"); + +failed: + g_free(*loopback); + *loopback = NULL; + return -1; +} + +int loop_detach(const gchar *loopback) +{ + int fd; + + fd = open(loopback, O_RDONLY | O_NOFOLLOW); + if (fd < 0) { + PERROR("open(%s)", loopback); + return 0; + } + if (!is_loop_device(fd) || !loop_is_attached(fd)) + goto failed; + if (ioctl(fd, LOOP_CLR_FD, 0)) { + PERROR("LOOP_CLR_FD"); + goto failed; + } + + close (fd); + return 1; + +failed: + close(fd); + return 0; +} + +gchar *loop_attach(int fd, const char *name) +{ + gchar *loopback = NULL; + int loopfd; + struct loop_info64 info; + + loopfd = loop_allocate(&loopback); + if (loopfd < 0) + return NULL; + if (ioctl(loopfd, LOOP_SET_FD, fd) < 0) { + PERROR("LOOP_SET_FD"); + goto failed; + } + + memset(&info, 0, sizeof(info)); + strncpy((char*)info.lo_file_name, name, LO_NAME_SIZE); + if (ioctl(loopfd, LOOP_SET_STATUS64, &info)) { + PERROR("LOOP_SET_STATUS64"); + goto failed; + } + + close(loopfd); + close(fd); + return loopback; +failed: + close(loopfd); + close(fd); + g_free(loopback); + return 0; +} + +int dm_setup(size_t sectors, const gchar *encryption_key, const char *name, + const gchar *device, const char *path) +{ + /* Mount loopback device with dm-crypt using the encryption key. */ + gchar *table = g_strdup_printf("0 %zu crypt " \ + "aes-cbc-essiv:sha256 %s " \ + "0 %s 0 " \ + "1 allow_discards", + sectors, + encryption_key, + device); + if (!table) { + PERROR("g_strdup_printf"); + return 0; + } + + const gchar *argv[] = { + "/sbin/dmsetup", + "create", name, + "--noudevrules", "--noudevsync", + "--table", table, + NULL + }; + + /* TODO(keescook): replace with call to libdevmapper. */ + if (runcmd(argv, NULL) != 0) { + g_free(table); + return 0; + } + g_free(table); + + /* Make sure the dm-crypt device showed up. */ + if (access(path, R_OK)) { + ERROR("%s does not exist", path); + return 0; + } + + return 1; +} + +void dm_teardown(const gchar *device) +{ + const char *argv[] = { + "/sbin/dmsetup", + "remove", device, + "--noudevrules", "--noudevsync", + NULL + }; + /* TODO(keescook): replace with call to libdevmapper. */ + runcmd(argv, NULL); +} + +char *dm_get_key(const gchar *device) +{ + gchar *output = NULL; + char *key; + int i; + const char *argv[] = { + "/sbin/dmsetup", + "table", "--showkeys", + device, + NULL + }; + /* TODO(keescook): replace with call to libdevmapper. */ + if (runcmd(argv, &output) != 0) + return NULL; + + /* Key is 4th field in the output. */ + for (i = 0, key = strtok(output, " "); + i < 4 && key; + ++i, key = strtok(NULL, " ")) { } + + /* Create a copy of the key and free the output buffer. */ + if (key) { + key = strdup(key); + g_free(output); + } + + return key; +} + +int sparse_create(const char *path, size_t size) +{ + int sparsefd; + + sparsefd = open(path, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW, + S_IRUSR | S_IWUSR); + if (sparsefd < 0) + goto out; + + if (ftruncate(sparsefd, size)) { + int saved_errno = errno; + + close(sparsefd); + unlink(path); + errno = saved_errno; + + sparsefd = -1; + } + +out: + return sparsefd; +} + +int filesystem_build(const char *device, size_t block_bytes, size_t blocks_min, + size_t blocks_max) +{ + int rc = 0; + + gchar *blocksize = g_strdup_printf("%zu", block_bytes); + if (!blocksize) { + PERROR("g_strdup_printf"); + goto out; + } + + gchar *blocks_str; + blocks_str = g_strdup_printf("%zu", blocks_min); + if (!blocks_str) { + PERROR("g_strdup_printf"); + goto free_blocksize; + } + + gchar *extended; + if (blocks_min < blocks_max) { + extended = g_strdup_printf("%s,resize=%zu", + kExt4ExtendedOptions, blocks_max); + } else { + extended = g_strdup_printf("%s", kExt4ExtendedOptions); + } + if (!extended) { + PERROR("g_strdup_printf"); + goto free_blocks_str; + } + + const gchar *mkfs[] = { + "/sbin/mkfs.ext4", + "-T", "default", + "-b", blocksize, + "-m", "0", + "-O", "^huge_file,^flex_bg", + "-E", extended, + device, + blocks_str, + NULL + }; + + rc = (runcmd(mkfs, NULL) == 0); + if (!rc) + goto free_extended; + + const gchar *tune2fs[] = { + "/sbin/tune2fs", + "-c", "0", + "-i", "0", + device, + NULL + }; + rc = (runcmd(tune2fs, NULL) == 0); + +free_extended: + g_free(extended); +free_blocks_str: + g_free(blocks_str); +free_blocksize: + g_free(blocksize); +out: + return rc; +} + +/* Spawns a filesystem resizing process. */ +void filesystem_resizer(const char *device, size_t blocks, size_t blocks_max) +{ + pid_t pid; + + /* Ignore resizing if we know the filesystem was built to max size. */ + if (blocks >= blocks_max) + return; + + fflush(NULL); + pid = fork(); + if (pid < 0) { + PERROR("fork"); + return; + } + if (pid != 0) { + INFO("Started filesystem resizing process %d.", pid); + return; + } + + if (setsid() < 0) { + PERROR("setsid"); + goto out; + } + + /* TODO(keescook): Read superblock to find out the current size of + * the filesystem (since statvfs does not report the correct value). + * For now, instead of doing multi-step resizing, just resize to the + * full size of the block device in one step. + */ + blocks = blocks_max; + + INFO_INIT("Resizing started in %d second steps.", kResizeStepSeconds); + + do { + gchar *blocks_str; + + sleep(kResizeStepSeconds); + + blocks += kResizeBlocks; + if (blocks > blocks_max) + blocks = blocks_max; + + blocks_str = g_strdup_printf("%zu", blocks); + if (!blocks_str) { + PERROR("g_strdup_printf"); + goto out; + } + + const gchar *resize[] = { + "/sbin/resize2fs", + "-f", + device, + blocks_str, + NULL + }; + + INFO("Resizing filesystem on %s to %zu.", device, blocks); + if (runcmd(resize, NULL)) { + ERROR("resize2fs failed"); + goto out; + } + g_free(blocks_str); + } while (blocks < blocks_max); + + INFO("Resizing finished."); +out: + exit(0); +} + +char *keyfile_read(const char *keyfile, uint8_t *system_key) +{ + char *key = NULL; + unsigned char *cipher = NULL; + gsize length; + uint8_t *plain = NULL; + int plain_length, final_len; + GError *error = NULL; + EVP_CIPHER_CTX ctx; + const EVP_CIPHER *algo = EVP_aes_256_cbc(); + + DEBUG("Reading keyfile %s", keyfile); + if (EVP_CIPHER_key_length(algo) != DIGEST_LENGTH) { + ERROR("cipher key size mismatch (got %d, want %d)", + EVP_CIPHER_key_length(algo), DIGEST_LENGTH); + goto out; + } + + if (access(keyfile, R_OK)) { + /* This file being missing is handled in caller, so + * do not emit error message. + */ + INFO("%s does not exist.", keyfile); + goto out; + } + + if (!g_file_get_contents(keyfile, (gchar **)&cipher, &length, + &error)) { + ERROR("Unable to read %s: %s", keyfile, error->message); + g_error_free(error); + goto out; + } + plain = malloc(length); + if (!plain) { + PERROR("malloc"); + goto free_cipher; + } + + DEBUG("Decrypting keyfile %s", keyfile); + /* Use the default IV. */ + if (!EVP_DecryptInit(&ctx, algo, system_key, NULL)) { + SSL_ERROR("EVP_DecryptInit"); + goto free_plain; + } + /* TODO(keescook): this is a heap overflow -- file size not checked. */ + if (!EVP_DecryptUpdate(&ctx, plain, &plain_length, cipher, length)) { + SSL_ERROR("EVP_DecryptUpdate"); + goto free_ctx; + } + if (!EVP_DecryptFinal(&ctx, plain+plain_length, &final_len)) { + SSL_ERROR("EVP_DecryptFinal"); + goto free_ctx; + } + plain_length += final_len; + + if (plain_length != DIGEST_LENGTH) { + ERROR("Decrypted encryption key length (%d) is not %d.", + plain_length, DIGEST_LENGTH); + goto free_ctx; + } + + debug_dump_hex("encryption key", plain, DIGEST_LENGTH); + + key = stringify_hex(plain, DIGEST_LENGTH); + +free_ctx: + EVP_CIPHER_CTX_cleanup(&ctx); +free_plain: + free(plain); +free_cipher: + g_free(cipher); +out: + DEBUG("key:%p", key); + return key; +} + +int keyfile_write(const char *keyfile, uint8_t *system_key, char *string) +{ + int rc = 0; + size_t length; + uint8_t plain[DIGEST_LENGTH]; + uint8_t *cipher = NULL; + int cipher_length, final_len; + GError *error = NULL; + EVP_CIPHER_CTX ctx; + const EVP_CIPHER *algo = EVP_aes_256_cbc(); + + DEBUG("Staring to process keyfile %s", keyfile); + if (EVP_CIPHER_key_length(algo) != DIGEST_LENGTH) { + ERROR("cipher key size mismatch (got %d, want %d)", + EVP_CIPHER_key_length(algo), DIGEST_LENGTH); + goto out; + } + + if (access(keyfile, R_OK) == 0) { + ERROR("%s already exists.", keyfile); + goto out; + } + + length = strlen(string); + if (length != sizeof(plain) * 2) { + ERROR("Encryption key string length (%zu) is not %zu.", + length, sizeof(plain) * 2); + goto out; + } + + length = sizeof(plain); + if (!hexify_string(string, plain, length)) { + ERROR("Failed to convert encryption key to byte array"); + goto out; + } + + debug_dump_hex("encryption key", plain, DIGEST_LENGTH); + + cipher = malloc(length + EVP_CIPHER_block_size(algo)); + if (!cipher) { + PERROR("malloc"); + goto out; + } + + DEBUG("Encrypting keyfile %s", keyfile); + /* Use the default IV. */ + if (!EVP_EncryptInit(&ctx, algo, system_key, NULL)) { + SSL_ERROR("EVP_EncryptInit"); + goto free_cipher; + } + if (!EVP_EncryptUpdate(&ctx, cipher, &cipher_length, + (unsigned char *)plain, length)) { + SSL_ERROR("EVP_EncryptUpdate"); + goto free_ctx; + } + if (!EVP_EncryptFinal(&ctx, cipher+cipher_length, &final_len)) { + SSL_ERROR("EVP_EncryptFinal"); + goto free_ctx; + } + length = cipher_length + final_len; + + DEBUG("Writing keyfile %s", keyfile); + if (!g_file_set_contents(keyfile, (gchar *)cipher, length, &error)) { + ERROR("Unable to write %s: %s", keyfile, error->message); + g_error_free(error); + goto free_ctx; + } + + rc = 1; + +free_ctx: + EVP_CIPHER_CTX_cleanup(&ctx); +free_cipher: + free(cipher); +out: + DEBUG("rc:%d", rc); + return rc; +} diff --git a/utility/mount-helpers.h b/utility/mount-helpers.h new file mode 100644 index 00000000..bc50fe54 --- /dev/null +++ b/utility/mount-helpers.h @@ -0,0 +1,40 @@ +/* Copyright (c) 2012 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. + * + * Header file for mount helpers. + */ +#ifndef _MOUNT_HELPERS_H_ +#define _MOUNT_HELPERS_H_ + +/* General utility functions. */ +size_t get_sectors(const char *device); +int remove_tree(const char *tree); +int runcmd(const gchar *argv[], gchar **output); +int same_vfs(const char *mnt_a, const char *mnt_b); +char *stringify_hex(uint8_t *binary, size_t length); +uint8_t *hexify_string(char *string, uint8_t *binary, size_t length); + +/* Loopback device attach/detach helpers. */ +gchar *loop_attach(int fd, const char *name); +int loop_detach(const gchar *loopback); + +/* Encrypted device mapper setup/teardown. */ +int dm_setup(size_t sectors, const gchar *encryption_key, const char *name, + const gchar *device, const char *path); +void dm_teardown(const gchar *device); +char *dm_get_key(const gchar *device); + +/* Sparse file creation. */ +int sparse_create(const char *path, size_t size); + +/* Filesystem creation. */ +int filesystem_build(const char *device, size_t block_bytes, size_t blocks_min, + size_t blocks_max); +void filesystem_resizer(const char *device, size_t blocks, size_t blocks_max); + +/* Encrypted keyfile handling. */ +char *keyfile_read(const char *keyfile, uint8_t *system_key); +int keyfile_write(const char *keyfile, uint8_t *system_key, char *plain); + +#endif /* _MOUNT_HELPERS_H_ */ |