blob: 694aef7d5f92818a10f6c29b6be8dfd624e7077f [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2022 Google LLC
*/
#include <vm_instance.h>
#include <dm/uclass.h>
#include <linux/bug.h>
#include <linux/err.h>
#include <log.h>
#include <openssl/aead.h>
#include <rng.h>
#define VM_INSTANCE_BLOCK_SIZE 512
#define VM_INSTANCE_MAX_BLOCK (S64_MAX / VM_INSTANCE_BLOCK_SIZE)
#define DISK_HEADER_MAX_VERSION 1
static const char vm_instance_hdr_magic[] = {
'A', 'n', 'd', 'r', 'o', 'i', 'd', '-', 'V', 'M', '-',
'i', 'n', 's', 't', 'a', 'n', 'c', 'e' };
struct vm_instance_hdr {
uint8_t magic[sizeof(vm_instance_hdr_magic)];
uint16_t version;
} __packed;
struct vm_instance_part_hdr {
struct uuid uuid;
uint64_t part_size;
} __packed;
static int read_bytes(struct AvbOps *ops, size_t block_num,
void *buf, size_t size)
{
AvbIOResult res;
size_t bytes_read;
int64_t offset;
if (block_num > VM_INSTANCE_MAX_BLOCK)
return -EINVAL;
offset = block_num * VM_INSTANCE_BLOCK_SIZE;
res = ops->read_from_partition(ops, CONFIG_VM_INSTANCE_PARTITION_NAME,
offset, size, buf, &bytes_read);
if (res != AVB_IO_RESULT_OK)
return -EIO;
if (bytes_read != size)
return -ENOENT;
return 0;
}
static int write_bytes(struct AvbOps *ops, size_t block_num,
const void *buf, size_t size)
{
AvbIOResult res;
int64_t offset;
if (block_num > VM_INSTANCE_MAX_BLOCK)
return -EINVAL;
offset = block_num * VM_INSTANCE_BLOCK_SIZE;
res = ops->write_to_partition(ops, CONFIG_VM_INSTANCE_PARTITION_NAME,
offset, size, buf);
if (res != AVB_IO_RESULT_OK)
return -EIO;
return 0;
}
static int read_and_verify_img_hdr_block(struct AvbOps *ops, size_t block_num)
{
struct vm_instance_hdr hdr;
int ret;
ret = read_bytes(ops, block_num, &hdr, sizeof(hdr));
if (ret)
return ret;
hdr.version = le16_to_cpu(hdr.version);
if (memcmp(hdr.magic, vm_instance_hdr_magic, sizeof(hdr.magic)))
return -EINVAL;
if (!hdr.version || hdr.version > DISK_HEADER_MAX_VERSION)
return -EINVAL;
return 0;
}
/**
* Search the partitions for one with the given UUID.
*
* If the partition is found, returns 0 and end_block_num is set to the first
* block of the partition contents. If the partition is not found, returns
* -ENOENT and end_block_num is set to the block where the next partition header
* is expected. Otherwise, returns an error code and end_block_num is not
* valid;
*/
static int find_partition(struct AvbOps *ops, size_t block_num,
const struct uuid *uuid,
struct vm_instance_part_hdr *hdr,
size_t *end_block_num)
{
const struct vm_instance_part_hdr zero_hdr = {};
size_t blocks;
int ret;
while (true) {
ret = read_bytes(ops, block_num, hdr, sizeof(*hdr));
if (ret == -ENOENT)
break;
else if (ret)
return ret;
/* An empty partition means the end of the file. */
if (memcmp(hdr, &zero_hdr, sizeof(zero_hdr)) == 0) {
ret = -ENOENT;
break;
}
/* The parition is valid, so check if it's the right one. */
block_num += 1;
if (memcmp(&hdr->uuid, uuid, sizeof(*uuid)) == 0) {
ret = 0;
break;
}
/* Skip payload. */
blocks = DIV_ROUND_UP(hdr->part_size, VM_INSTANCE_BLOCK_SIZE);
if (blocks > VM_INSTANCE_MAX_BLOCK - block_num)
return -EINVAL;
block_num += blocks;
}
*end_block_num = block_num;
return ret;
}
int vm_instance_load_entry(struct AvbOps *ops, const struct uuid *uuid,
const uint8_t *sealing_key, size_t sealing_key_size,
void *data, size_t data_size)
{
const EVP_AEAD *aead = EVP_aead_aes_256_gcm_randnonce();
EVP_AEAD_CTX ctx;
size_t block_num;
struct vm_instance_part_hdr hdr;
size_t max_sealed_size, sealed_size, opened_size;
uint8_t *sealed;
int ret;
sealed = NULL;
max_sealed_size = data_size + EVP_AEAD_max_overhead(aead);
if (!uuid)
return -EINVAL;
if (!EVP_AEAD_CTX_init(&ctx, aead, sealing_key, sealing_key_size,
EVP_AEAD_DEFAULT_TAG_LENGTH, /*engine=*/NULL))
return -EINVAL;
block_num = 0;
ret = read_and_verify_img_hdr_block(ops, block_num);
if (ret < 0)
goto out;
block_num = 1;
ret = find_partition(ops, block_num, uuid, &hdr, &block_num);
if (ret)
goto out;
sealed_size = hdr.part_size;
if (sealed_size > max_sealed_size) {
ret = -EINVAL;
goto out;
}
sealed = kmalloc(max_sealed_size, GFP_KERNEL);
if (!sealed) {
ret = -ENOMEM;
goto out;
}
ret = read_bytes(ops, block_num, sealed, sealed_size);
if (ret)
goto out;
if (!EVP_AEAD_CTX_open(&ctx, data, &opened_size, data_size,
/*nonce=*/NULL, /*nonce_len=*/0,
sealed, sealed_size,
/*ad=*/NULL, /*ad_len=*/0)) {
ret = -EINVAL;
goto out;
}
if (opened_size != data_size) {
ret = -EINVAL;
goto out;
}
out:
EVP_AEAD_CTX_cleanup(&ctx);
kfree(sealed);
return ret;
}
int vm_instance_save_entry(struct AvbOps *ops, const struct uuid *uuid,
const uint8_t *sealing_key, size_t sealing_key_size,
const void *data, size_t data_size)
{
const EVP_AEAD *aead = EVP_aead_aes_256_gcm_randnonce();
EVP_AEAD_CTX ctx;
size_t block_num;
struct vm_instance_part_hdr hdr;
int ret;
size_t sealed_size, max_sealed_size;
uint8_t *sealed;
sealed = NULL;
max_sealed_size = data_size + EVP_AEAD_max_overhead(aead);
if (!uuid)
return -EINVAL;
if (!EVP_AEAD_CTX_init(&ctx, aead, sealing_key, sealing_key_size,
EVP_AEAD_DEFAULT_TAG_LENGTH, /*engine=*/NULL))
return -EINVAL;
sealed = kmalloc(max_sealed_size, GFP_KERNEL);
if (!sealed) {
ret = -ENOMEM;
goto out;
}
if (!EVP_AEAD_CTX_seal(&ctx, sealed, &sealed_size, max_sealed_size,
/*nonce=*/NULL, /*nonce_len=*/0,
data, data_size,
/*ad=*/NULL, /*ad_len=*/0)) {
ret = -EINVAL;
goto out;
}
block_num = 0;
ret = read_and_verify_img_hdr_block(ops, block_num);
if (ret)
goto out;
block_num = 1;
ret = find_partition(ops, block_num, uuid, &hdr, &block_num);
if (ret == -ENOENT) {
hdr = (struct vm_instance_part_hdr){ .part_size = sealed_size };
memcpy(&hdr.uuid, uuid, sizeof(*uuid));
ret = write_bytes(ops, block_num, &hdr, sizeof(hdr));
block_num += 1;
}
if (ret)
goto out;
/* Check the payload size is still the same. */
if (hdr.part_size != sealed_size) {
ret = -EINVAL;
goto out;
}
ret = write_bytes(ops, block_num, sealed, sealed_size);
out:
EVP_AEAD_CTX_cleanup(&ctx);
kfree(sealed);
return ret;
}
int vm_instance_new_salt(uint8_t salt[VM_INSTANCE_SALT_SIZE])
{
struct udevice *dev;
int ret;
ret = uclass_get_device(UCLASS_RNG, 0, &dev);
if (ret) {
log_err("No RNG device.\n");
return ret;
}
ret = dm_rng_read(dev, salt, VM_INSTANCE_SALT_SIZE);
if (ret) {
log_err("No RNG.\n");
return ret;
}
return 0;
}