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:
parent
b7a2f8715e
commit
09fd125059
@ -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 <<EOF
|
||||
ID=debian
|
||||
VERSION_ID=13
|
||||
EOF
|
||||
|
||||
cat >repart.d/10-root.conf <<EOF
|
||||
[Partition]
|
||||
Type=root
|
||||
Format=erofs
|
||||
SizeMinBytes=100M
|
||||
SizeMaxBytes=100M
|
||||
Verity=data
|
||||
VerityMatchKey=root
|
||||
EOF
|
||||
|
||||
cat >repart.d/11-root-verity.conf <<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 <<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 > /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>
|
||||
|
@ -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.");
|
||||
|
@ -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() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user