repart: Add support for generating verity sig partitions

This commit is contained in:
Daan De Meyer 2022-09-11 10:49:24 +02:00
parent 8939d3351d
commit b456191d3c
5 changed files with 320 additions and 22 deletions

View file

@ -583,17 +583,21 @@
<varlistentry>
<term><varname>Verity=</varname></term>
<listitem><para>Takes one of <literal>off</literal>, <literal>data</literal> or
<literal>hash</literal>. Defaults to <literal>off</literal>. If set to <literal>off</literal> or
<literal>data</literal>, the partition is populated with content as specified by
<varname>CopyBlocks=</varname> or <varname>CopyFiles=</varname>. If set to <literal>hash</literal>,
the partition will be populated with verity hashes from a matching verity data partition. A matching
data partition is a partition with <varname>Verity=</varname> set to <literal>data</literal> and the
same verity match key (as configured with <varname>VerityMatchKey=</varname>). If not explicitly
configured, the data partition's UUID will be set to the first 128 bits of the verity root hash.
Similarly, if not configured, the hash partition's UUID will be set to the final 128 bits of the
verity root hash. The verity root hash itself will be included in the output of
<command>systemd-repart</command>.</para>
<listitem><para>Takes one of <literal>off</literal>, <literal>data</literal>,
<literal>hash</literal> or <literal>signature</literal>. Defaults to <literal>off</literal>. If set
to <literal>off</literal> or <literal>data</literal>, the partition is populated with content as
specified by <varname>CopyBlocks=</varname> or <varname>CopyFiles=</varname>. If set to
<literal>hash</literal>, the partition will be populated with verity hashes from the matching verity
data partition. If set to <literal>signature</literal>, The partition will be populated with a JSON
object containing a signature of the verity root hash of the matching verity hash partition.</para>
<para>A matching verity partition is a partition with the same verity match key (as configured with
<varname>VerityMatchKey=</varname>).</para>
<para>If not explicitly configured, the data partition's UUID will be set to the first 128
bits of the verity root hash. Similarly, if not configured, the hash partition's UUID will be set to
the final 128 bits of the verity root hash. The verity root hash itself will be included in the
output of <command>systemd-repart</command>.</para>
<para>This option has no effect if the partition already exists.</para>

View file

@ -3682,7 +3682,8 @@ if conf.get('ENABLE_REPART') == 1
link_with : [libshared],
dependencies : [threads,
libblkid,
libfdisk],
libfdisk,
libopenssl],
install_rpath : rootpkglibdir,
install : true,
install_dir : rootbindir)

View file

@ -40,6 +40,7 @@
#include "hexdecoct.h"
#include "hmac.h"
#include "id128-util.h"
#include "io-util.h"
#include "json.h"
#include "list.h"
#include "loop-util.h"
@ -48,6 +49,7 @@
#include "mkfs-util.h"
#include "mount-util.h"
#include "mountpoint-util.h"
#include "openssl-util.h"
#include "parse-argument.h"
#include "parse-helpers.h"
#include "pretty-print.h"
@ -76,6 +78,9 @@
/* Hard lower limit for new partition sizes */
#define HARD_MIN_SIZE 4096
/* We know up front we're never going to put more than this in a verity sig partition. */
#define VERITY_SIG_SIZE (HARD_MIN_SIZE * 4)
/* libfdisk takes off slightly more than 1M of the disk size when creating a GPT disk label */
#define GPT_METADATA_SIZE (1044*1024)
@ -113,6 +118,8 @@ static PagerFlags arg_pager_flags = 0;
static bool arg_legend = true;
static void *arg_key = NULL;
static size_t arg_key_size = 0;
static EVP_PKEY *arg_private_key = NULL;
static X509 *arg_certificate = NULL;
static char *arg_tpm2_device = NULL;
static uint32_t arg_tpm2_pcr_mask = UINT32_MAX;
static char *arg_tpm2_public_key = NULL;
@ -123,6 +130,8 @@ STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
STATIC_DESTRUCTOR_REGISTER(arg_definitions, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_key, erase_and_freep);
STATIC_DESTRUCTOR_REGISTER(arg_private_key, EVP_PKEY_freep);
STATIC_DESTRUCTOR_REGISTER(arg_certificate, X509_freep);
STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep);
STATIC_DESTRUCTOR_REGISTER(arg_tpm2_public_key, freep);
@ -143,6 +152,7 @@ typedef enum VerityMode {
VERITY_OFF,
VERITY_DATA,
VERITY_HASH,
VERITY_SIG,
_VERITY_MODE_MAX,
_VERITY_MODE_INVALID = -EINVAL,
} VerityMode;
@ -240,6 +250,7 @@ static const char *verity_mode_table[_VERITY_MODE_MAX] = {
[VERITY_OFF] = "off",
[VERITY_DATA] = "data",
[VERITY_HASH] = "hash",
[VERITY_SIG] = "signature",
};
#if HAVE_LIBCRYPTSETUP
@ -515,6 +526,9 @@ static uint64_t partition_min_size(const Context *context, const Partition *p) {
return p->current_size;
}
if (p->verity == VERITY_SIG)
return VERITY_SIG_SIZE;
sz = p->current_size != UINT64_MAX ? p->current_size : HARD_MIN_SIZE;
if (!PARTITION_EXISTS(p)) {
@ -556,6 +570,9 @@ static uint64_t partition_max_size(const Context *context, const Partition *p) {
return p->current_size;
}
if (p->verity == VERITY_SIG)
return VERITY_SIG_SIZE;
if (p->size_max == UINT64_MAX)
return UINT64_MAX;
@ -1548,7 +1565,8 @@ static int partition_read_definition(Partition *p, const char *path, const char
"VerityMatchKey= can only be set if Verity= is not \"%s\"",
verity_mode_to_string(p->verity));
if (p->verity == VERITY_HASH && (p->copy_files || p->copy_blocks_path || p->copy_blocks_auto || p->format || p->make_directories))
if (IN_SET(p->verity, VERITY_HASH, VERITY_SIG) &&
(p->copy_files || p->copy_blocks_path || p->copy_blocks_auto || p->format || p->make_directories))
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"CopyBlocks=/CopyFiles=/Format=/MakeDirectories= cannot be used with Verity=%s",
verity_mode_to_string(p->verity));
@ -1557,6 +1575,19 @@ static int partition_read_definition(Partition *p, const char *path, const char
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Encrypting verity hash/data partitions is not supported");
if (p->verity == VERITY_SIG && !arg_private_key)
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Verity signature partition requested but no private key provided (--private-key=)");
if (p->verity == VERITY_SIG && !arg_certificate)
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Verity signature partition requested but no PEM certificate provided (--certificate-file=)");
if (p->verity == VERITY_SIG && (p->size_min != UINT64_MAX || p->size_max != UINT64_MAX))
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"SizeMinBytes=/SizeMaxBytes= cannot be used with Verity=%s",
verity_mode_to_string(p->verity));
/* Verity partitions are read only, let's imply the RO flag hence, unless explicitly configured otherwise. */
if ((gpt_partition_type_is_root_verity(p->type_uuid) ||
gpt_partition_type_is_usr_verity(p->type_uuid)) &&
@ -1665,7 +1696,7 @@ static int context_read_definitions(
continue;
for (VerityMode mode = VERITY_OFF + 1; mode < _VERITY_MODE_MAX; mode++) {
Partition *q;
Partition *q = NULL;
if (p->verity == mode)
continue;
@ -1674,7 +1705,7 @@ static int context_read_definitions(
continue;
r = find_verity_sibling(context, p, mode, &q);
if (r == -ENXIO)
if (mode != VERITY_SIG && r == -ENXIO)
return log_syntax(NULL, LOG_ERR, p->definition_path, 1, SYNTHETIC_ERRNO(EINVAL),
"Missing verity %s partition for verity %s partition with VerityMatchKey=%s",
verity_mode_to_string(mode), verity_mode_to_string(p->verity), p->verity_match_key);
@ -1685,12 +1716,14 @@ static int context_read_definitions(
if (r < 0)
return r;
if (q->priority != p->priority)
return log_syntax(NULL, LOG_ERR, p->definition_path, 1, SYNTHETIC_ERRNO(EINVAL),
"Priority mismatch (%i != %i) for verity sibling partitions with VerityMatchKey=%s",
p->priority, q->priority, p->verity_match_key);
if (q) {
if (q->priority != p->priority)
return log_syntax(NULL, LOG_ERR, p->definition_path, 1, SYNTHETIC_ERRNO(EINVAL),
"Priority mismatch (%i != %i) for verity sibling partitions with VerityMatchKey=%s",
p->priority, q->priority, p->verity_match_key);
p->siblings[mode] = q;
p->siblings[mode] = q;
}
}
}
@ -3642,6 +3675,181 @@ static int context_verity_hash(Context *context) {
return 0;
}
static int parse_x509_certificate(const char *certificate, size_t certificate_size, X509 **ret) {
#if HAVE_OPENSSL
_cleanup_(X509_freep) X509 *cert = NULL;
_cleanup_(BIO_freep) BIO *cb = NULL;
assert(certificate);
assert(certificate_size > 0);
assert(ret);
cb = BIO_new_mem_buf(certificate, certificate_size);
if (!cb)
return log_oom();
cert = PEM_read_bio_X509(cb, NULL, NULL, NULL);
if (!cert)
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Failed to parse X.509 certificate: %s",
ERR_error_string(ERR_get_error(), NULL));
if (ret)
*ret = TAKE_PTR(cert);
return 0;
#else
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "openssl is not supported, cannot parse X509 certificate.");
#endif
}
static int parse_private_key(const char *key, size_t key_size, EVP_PKEY **ret) {
#if HAVE_OPENSSL
_cleanup_(BIO_freep) BIO *kb = NULL;
_cleanup_(EVP_PKEY_freep) EVP_PKEY *pk = NULL;
assert(key);
assert(key_size > 0);
assert(ret);
kb = BIO_new_mem_buf(key, key_size);
if (!kb)
return log_oom();
pk = PEM_read_bio_PrivateKey(kb, NULL, NULL, NULL);
if (!pk)
return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse PEM private key: %s",
ERR_error_string(ERR_get_error(), NULL));
if (ret)
*ret = TAKE_PTR(pk);
return 0;
#else
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "openssl is not supported, cannot parse private key.");
#endif
}
static int sign_verity_roothash(
const uint8_t *roothash,
size_t roothash_size,
uint8_t **ret_signature,
size_t *ret_signature_size) {
#if HAVE_OPENSSL
_cleanup_(BIO_freep) BIO *rb = NULL;
_cleanup_(PKCS7_freep) PKCS7 *p7 = NULL;
_cleanup_free_ char *hex = NULL;
_cleanup_free_ uint8_t *sig = NULL;
int sigsz;
assert(roothash);
assert(roothash_size > 0);
assert(ret_signature);
assert(ret_signature_size);
hex = hexmem(roothash, roothash_size);
if (!hex)
return log_oom();
rb = BIO_new_mem_buf(hex, -1);
if (!rb)
return log_oom();
p7 = PKCS7_sign(arg_certificate, arg_private_key, NULL, rb, PKCS7_DETACHED|PKCS7_NOATTR|PKCS7_BINARY);
if (!p7)
return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to calculate PKCS7 signature: %s",
ERR_error_string(ERR_get_error(), NULL));
sigsz = i2d_PKCS7(p7, &sig);
if (sigsz < 0)
return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to convert PKCS7 signature to DER: %s",
ERR_error_string(ERR_get_error(), NULL));
*ret_signature = TAKE_PTR(sig);
*ret_signature_size = sigsz;
return 0;
#else
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "openssl is not supported, cannot setup verity signature: %m");
#endif
}
static int context_verity_sig(Context *context) {
int fd = -1, r;
assert(context);
LIST_FOREACH(partitions, p, context->partitions) {
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
_cleanup_free_ uint8_t *sig = NULL;
_cleanup_free_ char *text = NULL;
Partition *hp;
uint8_t fp[X509_FINGERPRINT_SIZE];
size_t sigsz, padsz;
if (p->dropped)
continue;
if (PARTITION_EXISTS(p))
continue;
if (p->verity != VERITY_SIG)
continue;
assert_se(hp = p->siblings[VERITY_HASH]);
assert(!hp->dropped);
assert(arg_certificate);
if (fd < 0)
assert_se((fd = fdisk_get_devfd(context->fdisk_context)) >= 0);
r = sign_verity_roothash(hp->roothash, hp->roothash_size, &sig, &sigsz);
if (r < 0)
return r;
r = x509_fingerprint(arg_certificate, fp);
if (r < 0)
return log_error_errno(r, "Unable to calculate X509 certificate fingerprint: %m");
r = json_build(&v,
JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("rootHash", JSON_BUILD_HEX(hp->roothash, hp->roothash_size)),
JSON_BUILD_PAIR(
"certificateFingerprint",
JSON_BUILD_HEX(fp, sizeof(fp))
),
JSON_BUILD_PAIR("signature", JSON_BUILD_BASE64(sig, sigsz))
)
);
if (r < 0)
return log_error_errno(r, "Failed to build JSON object: %m");
r = json_variant_format(v, 0, &text);
if (r < 0)
return log_error_errno(r, "Failed to format JSON object: %m");
padsz = round_up_size(strlen(text), 4096);
assert_se(padsz <= p->new_size);
r = strgrowpad0(&text, padsz);
if (r < 0)
return log_error_errno(r, "Failed to pad string to %s", FORMAT_BYTES(padsz));
if (lseek(fd, p->offset, SEEK_SET) == (off_t) -1)
return log_error_errno(errno, "Failed to seek to partition offset: %m");
r = loop_write(fd, text, padsz, /*do_poll=*/ false);
if (r < 0)
return log_error_errno(r, "Failed to write verity signature to partition: %m");
if (fsync(fd) < 0)
return log_error_errno(errno, "Failed to synchronize verity signature JSON: %m");
}
return 0;
}
static int partition_acquire_uuid(Context *context, Partition *p, sd_id128_t *ret) {
struct {
sd_id128_t type_uuid;
@ -3787,7 +3995,7 @@ static int context_acquire_partition_uuids_and_labels(Context *context) {
if (!sd_id128_is_null(p->current_uuid))
p->new_uuid = p->current_uuid; /* Never change initialized UUIDs */
else if (!p->new_uuid_is_set && p->verity == VERITY_OFF) {
else if (!p->new_uuid_is_set && !IN_SET(p->verity, VERITY_DATA, VERITY_HASH)) {
/* Not explicitly set by user! */
r = partition_acquire_uuid(context, p, &p->new_uuid);
if (r < 0)
@ -4205,6 +4413,10 @@ static int context_write_partition_table(
if (r < 0)
return r;
r = context_verity_sig(context);
if (r < 0)
return r;
r = context_mangle_partitions(context);
if (r < 0)
return r;
@ -4747,6 +4959,10 @@ static int help(void) {
" --image=PATH Operate relative to image file\n"
" --definitions=DIR Find partition definitions in specified directory\n"
" --key-file=PATH Key to use when encrypting partitions\n"
" --private-key=PATH Private key to use when generating verity roothash\n"
" signatures\n"
" --certificate=PATH PEM certificate to use when generating verity\n"
" roothash signatures\n"
" --tpm2-device=PATH Path to TPM2 device node to use\n"
" --tpm2-pcrs=PCR1+PCR2+PCR3+…\n"
" TPM2 PCR indexes to use for TPM2 enrollment\n"
@ -4787,6 +5003,8 @@ static int parse_argv(int argc, char *argv[]) {
ARG_SIZE,
ARG_JSON,
ARG_KEY_FILE,
ARG_PRIVATE_KEY,
ARG_CERTIFICATE,
ARG_TPM2_DEVICE,
ARG_TPM2_PCRS,
ARG_TPM2_PUBLIC_KEY,
@ -4812,6 +5030,8 @@ static int parse_argv(int argc, char *argv[]) {
{ "size", required_argument, NULL, ARG_SIZE },
{ "json", required_argument, NULL, ARG_JSON },
{ "key-file", required_argument, NULL, ARG_KEY_FILE },
{ "private-key", required_argument, NULL, ARG_PRIVATE_KEY },
{ "certificate", required_argument, NULL, ARG_CERTIFICATE },
{ "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE },
{ "tpm2-pcrs", required_argument, NULL, ARG_TPM2_PCRS },
{ "tpm2-public-key", required_argument, NULL, ARG_TPM2_PUBLIC_KEY },
@ -4985,6 +5205,46 @@ static int parse_argv(int argc, char *argv[]) {
break;
}
case ARG_PRIVATE_KEY: {
_cleanup_(erase_and_freep) char *k = NULL;
size_t n = 0;
r = read_full_file_full(
AT_FDCWD, optarg, UINT64_MAX, SIZE_MAX,
READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET,
NULL,
&k, &n);
if (r < 0)
return log_error_errno(r, "Failed to read key file '%s': %m", optarg);
EVP_PKEY_free(arg_private_key);
arg_private_key = NULL;
r = parse_private_key(k, n, &arg_private_key);
if (r < 0)
return r;
break;
}
case ARG_CERTIFICATE: {
_cleanup_free_ char *cert = NULL;
size_t n = 0;
r = read_full_file_full(
AT_FDCWD, optarg, UINT64_MAX, SIZE_MAX,
READ_FULL_FILE_CONNECT_SOCKET,
NULL,
&cert, &n);
if (r < 0)
return log_error_errno(r, "Failed to read certificate file '%s': %m", optarg);
X509_free(arg_certificate);
arg_certificate = NULL;
r = parse_x509_certificate(cert, n, &arg_certificate);
if (r < 0)
return r;
break;
}
case ARG_TPM2_DEVICE: {
_cleanup_free_ char *device = NULL;

View file

@ -10,6 +10,9 @@ TEST_DESCRIPTION="test systemd-repart"
test_append_files() {
if ! get_bool "${TEST_NO_QEMU:=}"; then
install_dmevent
if command -v openssl >/dev/null 2>&1; then
inst_binary openssl
fi
instmods dm_verity =md
generate_module_dependencies
fi

View file

@ -726,17 +726,47 @@ Verity=hash
VerityMatchKey=root
EOF
cat >"$defs/verity-sig.conf" <<EOF
[Partition]
Type=root-${architecture}-verity-sig
Verity=signature
VerityMatchKey=root
EOF
# Unfortunately OpenSSL insists on reading some config file, hence provide one with mostly placeholder contents
cat >> "$defs/verity.openssl.cnf" <<EOF
[ req ]
prompt = no
distinguished_name = req_distinguished_name
[ req_distinguished_name ]
C = DE
ST = Test State
L = Test Locality
O = Org Name
OU = Org Unit Name
CN = Common Name
emailAddress = test@email.com
EOF
openssl req -config "$defs/verity.openssl.cnf" -new -x509 -newkey rsa:1024 -keyout "$defs/verity.key" -out "$defs/verity.crt" -days 365 -nodes
mkdir -p /run/verity.d
ln -s "$defs/verity.crt" /run/verity.d/ok.crt
output=$(systemd-repart --definitions="$defs" \
--seed="$seed" \
--dry-run=no \
--empty=create \
--size=auto \
--json=pretty \
--private-key="$defs/verity.key" \
--certificate="$defs/verity.crt" \
"$imgs/verity")
roothash=$(jq -r ".[] | select(.type == \"root-${architecture}-verity\") | .roothash" <<< "$output")
# Check that we can dissect, mount and unmount a repart verity image.
# Check that we can dissect, mount and unmount a repart verity image.
systemd-dissect "$imgs/verity" --root-hash "$roothash"
systemd-dissect "$imgs/verity" --root-hash "$roothash" -M "$imgs/mnt"