1
0
mirror of https://github.com/systemd/systemd.git synced 2025-03-14 04:58:28 +03:00

repart: add parameter to attach offline verity signatures

Add --join-signature=hash:sig - when a verity signature partition
has been deferred in a previous run, this allows attaching a signature
that was created offline, for example on a build system like OBS where
the private key is not available to the build process.

Can be specified multiple times, the right partition to act upon will
be selected by matching the data+verity partitions UUIDs with the
provided roothash(es)
This commit is contained in:
Luca Boccassi 2025-02-03 16:05:46 +01:00
parent b7a2f8715e
commit 09fd125059
3 changed files with 256 additions and 17 deletions

View File

@ -388,6 +388,23 @@
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--join-signature=</option></term>
<listitem><para>Specifies a colon-separated tuple with a hex-encoded top-level Verity hash of a
<varname>Verity=hash</varname> partition as first element, and a PKCS7 signature of the roothash
as a path to a DER-encoded signature file, or as an ASCII base64 string encoding of a DER-encoded
signature prefixed by <literal>base64:</literal>. To be used on a pre-existing image that was
created with a parameter such as <option>--defer-partitions=root-verity-sig</option>, in order to
allow implementing offline signing of the verity signature partition.</para>
<para>This is an alternative to online signing using parameters such as
<option>--private-key=</option>, for build systems where the private key for production signing is
not available in the same context where content is created.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--tpm2-device=</option></term>
<term><option>--tpm2-pcrs=</option></term>
@ -703,6 +720,67 @@ systemd-sysext refresh</programlisting>
<citerefentry><refentrytitle>systemd-sysext</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para>
</example>
<example>
<title>Generate a dm-verity signature offline and append it to a pre-built image</title>
<para>The following creates an image with dm-verity metadata, signs it separately to simulate an
offline signing system, and then appends the signature to the image:</para>
<programlisting>mkdir -p repart.d/ /tmp/tree/usr/lib/
cat >/tmp/tree/usr/lib/os-release &lt;&lt;EOF
ID=debian
VERSION_ID=13
EOF
cat >repart.d/10-root.conf &lt;&lt;EOF
[Partition]
Type=root
Format=erofs
SizeMinBytes=100M
SizeMaxBytes=100M
Verity=data
VerityMatchKey=root
EOF
cat >repart.d/11-root-verity.conf &lt;&lt;EOF
[Partition]
Type=root-verity
Label=%o_%w_verity
Verity=hash
VerityMatchKey=root
SizeMinBytes=400M
SizeMaxBytes=400M
EOF
cat >repart.d/12-root-verity-sig.conf &lt;&lt;EOF
[Partition]
Type=root-verity-sig
Label=%o_%w_verity_sig
Verity=signature
VerityMatchKey=root
EOF
systemd-repart --definitions repart.d \
--defer-partitions=root-verity-sig \
--copy-source /tmp/tree/ \
--empty create --size 600M \
--json=short \
/tmp/img.raw | | jq --raw-output0 .[-1].roothash &gt; /tmp/img.roothash
openssl smime -sign -in /tmp/img.roothash \
-inkey privkey.pem \
-signer cert.crt \
-noattr -binary -outform der \
-out /tmp/img.roothash.p7s
systemd-repart --definitions repart.d \
--dry-run=no --root /tmp/tree/ \
--join-signature "$(cat /tmp/img.roothash):/tmp/img.roothash.p7s" \
--certificate cert.crt \
/tmp/img.raw</programlisting>
</example>
</refsect1>
<refsect1>

View File

@ -179,6 +179,7 @@ static char *arg_copy_source = NULL;
static char *arg_make_ddi = NULL;
static char *arg_generate_fstab = NULL;
static char *arg_generate_crypttab = NULL;
static Set *arg_verity_settings = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_node, freep);
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
@ -202,6 +203,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_copy_source, freep);
STATIC_DESTRUCTOR_REGISTER(arg_make_ddi, freep);
STATIC_DESTRUCTOR_REGISTER(arg_generate_fstab, freep);
STATIC_DESTRUCTOR_REGISTER(arg_generate_crypttab, freep);
STATIC_DESTRUCTOR_REGISTER(arg_verity_settings, set_freep);
typedef struct FreeArea FreeArea;
@ -5038,11 +5040,34 @@ static int sign_verity_roothash(
#endif
}
static const VeritySettings *lookup_verity_settings_by_uuid_pair(sd_id128_t data_uuid, sd_id128_t hash_uuid) {
uint8_t root_hash_key[sizeof(sd_id128_t) * 2];
if (sd_id128_is_null(data_uuid) || sd_id128_is_null(hash_uuid))
return NULL;
/* As per the https://uapi-group.org/specifications/specs/discoverable_partitions_specification/ the
* UUIDs of the data and verity partitions are respectively the first and second halves of the
* dm-verity roothash, so we can use them to match the signature to the right partition. */
memcpy(root_hash_key, data_uuid.bytes, sizeof(sd_id128_t));
memcpy(root_hash_key + sizeof(sd_id128_t), hash_uuid.bytes, sizeof(sd_id128_t));
VeritySettings key = {
.root_hash = &root_hash_key,
.root_hash_size = sizeof(root_hash_key),
};
return set_get(arg_verity_settings, &key);
}
static int partition_format_verity_sig(Context *context, Partition *p) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
_cleanup_(iovec_done) struct iovec sig = {};
_cleanup_(iovec_done) struct iovec sig_free = {};
_cleanup_free_ char *text = NULL, *hint = NULL;
Partition *hp;
const VeritySettings *verity_settings;
struct iovec roothash, sig;
Partition *hp, *rp;
uint8_t fp[X509_FINGERPRINT_SIZE];
int whole_fd, r;
@ -5054,24 +5079,42 @@ static int partition_format_verity_sig(Context *context, Partition *p) {
if (PARTITION_EXISTS(p))
return 0;
if (!context->private_key)
assert_se(hp = p->siblings[VERITY_HASH]);
assert(!hp->dropped);
assert_se(rp = p->siblings[VERITY_DATA]);
assert(!rp->dropped);
verity_settings = lookup_verity_settings_by_uuid_pair(rp->current_uuid, hp->current_uuid);
if (!context->private_key && !verity_settings)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Verity signature partition signing requested but no private key provided (--private-key=).");
if (!context->certificate)
if (!context->certificate && !verity_settings)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Verity signature partition signing requested but no PEM certificate provided (--certificate=).");
(void) partition_hint(p, context->node, &hint);
assert_se(hp = p->siblings[VERITY_HASH]);
assert(!hp->dropped);
assert_se((whole_fd = fdisk_get_devfd(context->fdisk_context)) >= 0);
r = sign_verity_roothash(&hp->roothash, context->certificate, context->private_key, &sig);
if (r < 0)
return r;
if (verity_settings) {
sig = (struct iovec) {
.iov_base = verity_settings->root_hash_sig,
.iov_len = verity_settings->root_hash_sig_size,
};
roothash = (struct iovec) {
.iov_base = verity_settings->root_hash,
.iov_len = verity_settings->root_hash_size,
};
} else {
r = sign_verity_roothash(&hp->roothash, context->certificate, context->private_key, &sig_free);
if (r < 0)
return r;
sig = sig_free;
roothash = hp->roothash;
}
r = x509_fingerprint(context->certificate, fp);
if (r < 0)
@ -5079,7 +5122,7 @@ static int partition_format_verity_sig(Context *context, Partition *p) {
r = sd_json_buildo(
&v,
SD_JSON_BUILD_PAIR("rootHash", SD_JSON_BUILD_HEX(hp->roothash.iov_base, hp->roothash.iov_len)),
SD_JSON_BUILD_PAIR("rootHash", SD_JSON_BUILD_HEX(roothash.iov_base, roothash.iov_len)),
SD_JSON_BUILD_PAIR("certificateFingerprint", SD_JSON_BUILD_HEX(fp, sizeof(fp))),
SD_JSON_BUILD_PAIR("signature", JSON_BUILD_IOVEC_BASE64(&sig)));
if (r < 0)
@ -5137,9 +5180,6 @@ static int context_copy_blocks(Context *context) {
LIST_FOREACH(partitions, p, context->partitions) {
_cleanup_(partition_target_freep) PartitionTarget *t = NULL;
if (p->copy_blocks_fd < 0)
continue;
if (p->dropped)
continue;
@ -5149,6 +5189,13 @@ static int context_copy_blocks(Context *context) {
if (partition_type_defer(&p->type))
continue;
/* For offline signing case */
if (!set_isempty(arg_verity_settings) && IN_SET(p->type.designator, PARTITION_ROOT_VERITY_SIG, PARTITION_USR_VERITY_SIG))
return partition_format_verity_sig(context, p);
if (p->copy_blocks_fd < 0)
continue;
assert(p->new_size != UINT64_MAX);
size_t extra = p->encrypt != ENCRYPT_OFF ? LUKS2_METADATA_KEEP_FREE : 0;
@ -5995,11 +6042,15 @@ static int context_mkfs(Context *context) {
if (!p->format)
continue;
/* Minimized partitions will use the copy blocks logic so skip those here. */
if (p->copy_blocks_fd >= 0)
if (partition_type_defer(&p->type))
continue;
if (partition_type_defer(&p->type))
/* For offline signing case */
if (!set_isempty(arg_verity_settings) && IN_SET(p->type.designator, PARTITION_ROOT_VERITY_SIG, PARTITION_USR_VERITY_SIG))
return partition_format_verity_sig(context, p);
/* Minimized partitions will use the copy blocks logic so skip those here. */
if (p->copy_blocks_fd >= 0)
continue;
assert(p->offset != UINT64_MAX);
@ -7821,6 +7872,67 @@ static int parse_partition_types(const char *p, GptPartitionType **partitions, s
return 0;
}
static int parse_join_signature(const char *p, Set **verity_settings_map) {
_cleanup_(verity_settings_freep) VeritySettings *verity_settings = NULL;
_cleanup_free_ char *root_hash = NULL;
_cleanup_free_ void *content = NULL;
const char *signature;
size_t len;
int r;
assert(p);
assert(verity_settings_map);
r = extract_first_word(&p, &root_hash, ":", 0);
if (r < 0)
return log_error_errno(r, "Failed to parse signature parameter '%s': %m", p);
if (!p)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected hash:sig");
if ((signature = startswith(p, "base64:"))) {
r = unbase64mem(signature, &content, &len);
if (r < 0)
return log_error_errno(r, "Failed to parse root hash signature '%s': %m", signature);
} else {
r = read_full_file(p, (char**) &content, &len);
if (r < 0)
return log_error_errno(r, "Failed to read root hash signature file '%s': %m", p);
}
if (len == 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty verity signature specified.");
if (len > VERITY_SIG_SIZE)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Verity signatures larger than %llu are not allowed.",
VERITY_SIG_SIZE);
verity_settings = new(VeritySettings, 1);
if (!verity_settings)
return log_oom();
*verity_settings = (VeritySettings) {
.root_hash_sig = TAKE_PTR(content),
.root_hash_sig_size = len,
};
r = unhexmem(root_hash, &content, &len);
if (r < 0)
return log_error_errno(r, "Failed to parse root hash '%s': %m", root_hash);
if (len < sizeof(sd_id128_t))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Root hash must be at least 128-bit long: %s",
root_hash);
verity_settings->root_hash = TAKE_PTR(content);
verity_settings->root_hash_size = len;
r = set_ensure_put(verity_settings_map, &verity_settings_hash_ops, verity_settings);
if (r < 0)
return log_error_errno(r, "Failed to add entry to hashmap: %m");
TAKE_PTR(verity_settings);
return 0;
}
static int help(void) {
_cleanup_free_ char *link = NULL;
int r;
@ -7878,6 +7990,11 @@ static int help(void) {
" Specify how to interpret the certificate from\n"
" --certificate=. Allows the certificate to be loaded\n"
" from an OpenSSL provider\n"
" --join-signature=HASH:SIG\n"
" Specify root hash and pkcs7 signature of root hash for\n"
" verity as a tuple of hex encoded hash and a DER\n"
" encoded PKCS7, either as a path to a file or as an\n"
" ASCII base64 encoded string prefixed by 'base64:'\n"
"\n%3$sEncryption:%4$s\n"
" --key-file=PATH Key to use when encrypting partitions\n"
" --tpm2-device=PATH Path to TPM2 device node to use\n"
@ -7967,6 +8084,7 @@ static int parse_argv(int argc, char *argv[], X509 **ret_certificate, EVP_PKEY *
ARG_GENERATE_FSTAB,
ARG_GENERATE_CRYPTTAB,
ARG_LIST_DEVICES,
ARG_JOIN_SIGNATURE,
};
static const struct option options[] = {
@ -8012,6 +8130,7 @@ static int parse_argv(int argc, char *argv[], X509 **ret_certificate, EVP_PKEY *
{ "generate-fstab", required_argument, NULL, ARG_GENERATE_FSTAB },
{ "generate-crypttab", required_argument, NULL, ARG_GENERATE_CRYPTTAB },
{ "list-devices", no_argument, NULL, ARG_LIST_DEVICES },
{ "join-signature", required_argument, NULL, ARG_JOIN_SIGNATURE },
{}
};
@ -8410,6 +8529,12 @@ static int parse_argv(int argc, char *argv[], X509 **ret_certificate, EVP_PKEY *
return 0;
case ARG_JOIN_SIGNATURE:
r = parse_join_signature(optarg, &arg_verity_settings);
if (r < 0)
return r;
break;
case '?':
return -EINVAL;
@ -8453,6 +8578,9 @@ static int parse_argv(int argc, char *argv[], X509 **ret_certificate, EVP_PKEY *
if (arg_empty == EMPTY_UNSET) /* default to refuse mode, if not otherwise specified */
arg_empty = EMPTY_REFUSE;
if (!set_isempty(arg_verity_settings) && !arg_certificate)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Verity signature specified without --certificate=.");
if (arg_factory_reset > 0 && IN_SET(arg_empty, EMPTY_FORCE, EMPTY_REQUIRE, EMPTY_CREATE))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Combination of --factory-reset=yes and --empty=force/--empty=require/--empty=create is invalid.");

View File

@ -897,6 +897,34 @@ EOF
assert_eq "$drh" "$hrh"
assert_eq "$hrh" "$srh"
# Check that offline signing works and the resulting image is valid
output=$(systemd-repart --offline="$OFFLINE" \
--definitions="$defs" \
--seed="$seed" \
--dry-run=no \
--empty=create \
--size=auto \
--json=pretty \
--defer-partitions=root-${architecture}-verity-sig \
"$imgs/offline")
offline_drh=$(jq -r ".[] | select(.type == \"root-${architecture}\") | .roothash" <<<"$output")
echo -n "$offline_drh" | \
openssl smime -sign -in /dev/stdin \
-inkey "$defs/verity.key" \
-signer "$defs/verity.crt" \
-noattr -binary -outform der \
-out "$imgs/offline.roothash.p7s"
systemd-repart --offline "$OFFLINE" \
--definitions "$defs" \
--dry-run no \
--join-signature "$offline_drh:$imgs/offline.roothash.p7s" \
--certificate "$defs/verity.crt" \
"$imgs/offline"
# Check that we can dissect, mount and unmount a repart verity image. (and that the image UUID is deterministic)
if systemd-detect-virt --quiet --container; then
@ -908,6 +936,11 @@ EOF
systemd-dissect "$imgs/verity" --root-hash "$drh" --json=short | grep -q '"imageUuid":"1d2ce291-7cce-4f7d-bc83-fdb49ad74ebd"'
systemd-dissect "$imgs/verity" --root-hash "$drh" -M "$imgs/mnt"
systemd-dissect -U "$imgs/mnt"
systemd-dissect "$imgs/offline" --root-hash "$offline_drh"
systemd-dissect "$imgs/offline" --root-hash "$offline_drh" --json=short | grep -q '"imageUuid":"1d2ce291-7cce-4f7d-bc83-fdb49ad74ebd"'
systemd-dissect "$imgs/offline" --root-hash "$offline_drh" -M "$imgs/mnt"
systemd-dissect -U "$imgs/mnt"
}
testcase_verity_explicit_block_size() {