summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--man/repart.d.xml26
-rw-r--r--meson.build3
-rw-r--r--src/partition/repart.c278
-rwxr-xr-xtest/TEST-58-REPART/test.sh3
-rwxr-xr-xtest/units/testsuite-58.sh32
5 files changed, 320 insertions, 22 deletions
diff --git a/man/repart.d.xml b/man/repart.d.xml
index 280ce6b8ea..eec48fb569 100644
--- a/man/repart.d.xml
+++ b/man/repart.d.xml
@@ -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>
diff --git a/meson.build b/meson.build
index 6022617832..a20e94c55a 100644
--- a/meson.build
+++ b/meson.build
@@ -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)
diff --git a/src/partition/repart.c b/src/partition/repart.c
index 1dbe045bd2..b03928d823 100644
--- a/src/partition/repart.c
+++ b/src/partition/repart.c
@@ -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;
diff --git a/test/TEST-58-REPART/test.sh b/test/TEST-58-REPART/test.sh
index c2920f9c22..31c5e67e6a 100755
--- a/test/TEST-58-REPART/test.sh
+++ b/test/TEST-58-REPART/test.sh
@@ -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
diff --git a/test/units/testsuite-58.sh b/test/units/testsuite-58.sh
index 313580f862..f41069ee04 100755
--- a/test/units/testsuite-58.sh
+++ b/test/units/testsuite-58.sh
@@ -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"