diff --git a/man/systemd-repart.xml b/man/systemd-repart.xml
index b4290108bc..2e11c45993 100644
--- a/man/systemd-repart.xml
+++ b/man/systemd-repart.xml
@@ -388,6 +388,23 @@
+
+
+
+ Specifies a colon-separated tuple with a hex-encoded top-level Verity hash of a
+ Verity=hash 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 base64:. To be used on a pre-existing image that was
+ created with a parameter such as , in order to
+ allow implementing offline signing of the verity signature partition.
+
+ This is an alternative to online signing using parameters such as
+ , for build systems where the private key for production signing is
+ not available in the same context where content is created.
+
+
+
+
@@ -703,6 +720,67 @@ systemd-sysext refresh
systemd-sysext8.
+
+ Generate a dm-verity signature offline and append it to a pre-built image
+
+ 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:
+
+ 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
+
+
diff --git a/src/repart/repart.c b/src/repart/repart.c
index f2bc5bc80e..51af029769 100644
--- a/src/repart/repart.c
+++ b/src/repart/repart.c
@@ -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.");
diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c
index 5ab0f9fe18..724aedc532 100644
--- a/src/shared/dissect-image.c
+++ b/src/shared/dissect-image.c
@@ -3172,6 +3172,33 @@ void verity_settings_done(VeritySettings *v) {
v->data_path = mfree(v->data_path);
}
+VeritySettings* verity_settings_free(VeritySettings *v) {
+ if (!v)
+ return NULL;
+
+ verity_settings_done(v);
+ return mfree(v);
+}
+
+void verity_settings_hash_func(const VeritySettings *s, struct siphash *state) {
+ assert(s);
+
+ siphash24_compress_typesafe(s->root_hash_size, state);
+ siphash24_compress(s->root_hash, s->root_hash_size, state);
+}
+
+int verity_settings_compare_func(const VeritySettings *x, const VeritySettings *y) {
+ int r;
+
+ r = CMP(x->root_hash_size, y->root_hash_size);
+ if (r != 0)
+ return r;
+
+ return memcmp(x->root_hash, y->root_hash, x->root_hash_size);
+}
+
+DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(verity_settings_hash_ops, VeritySettings, verity_settings_hash_func, verity_settings_compare_func, VeritySettings, verity_settings_free);
+
int verity_settings_load(
VeritySettings *verity,
const char *image,
diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h
index c001915e89..9de93039da 100644
--- a/src/shared/dissect-image.h
+++ b/src/shared/dissect-image.h
@@ -211,6 +211,12 @@ static inline bool verity_settings_set(const VeritySettings *settings) {
}
void verity_settings_done(VeritySettings *verity);
+VeritySettings* verity_settings_free(VeritySettings *v);
+void verity_settings_hash_func(const VeritySettings *s, struct siphash *state);
+int verity_settings_compare_func(const VeritySettings *x, const VeritySettings *y);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(VeritySettings*, verity_settings_free);
+extern const struct hash_ops verity_settings_hash_ops;
static inline bool verity_settings_data_covers(const VeritySettings *verity, PartitionDesignator d) {
/* Returns true if the verity settings contain sufficient information to cover the specified partition */
diff --git a/test/units/TEST-58-REPART.sh b/test/units/TEST-58-REPART.sh
index b3181cea99..44a864fad0 100755
--- a/test/units/TEST-58-REPART.sh
+++ b/test/units/TEST-58-REPART.sh
@@ -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() {