]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
repart: add parameter to attach offline verity signatures 36405/head
authorLuca Boccassi <luca.boccassi@gmail.com>
Mon, 3 Feb 2025 15:05:46 +0000 (16:05 +0100)
committerLuca Boccassi <luca.boccassi@gmail.com>
Wed, 19 Feb 2025 16:26:05 +0000 (16:26 +0000)
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)

man/systemd-repart.xml
src/repart/repart.c
test/units/TEST-58-REPART.sh

index b4290108bc76105d1e997abaae2a9d302906c810..2e11c45993a0eef7a74455a58a2bef9b95adfc85 100644 (file)
         <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>
index f2bc5bc80ec49e13ab6a371f0aac2e9ee5637509..51af02976978fd4a6aa6b8dad52a6691bc6f5a3c 100644 (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.");
index b3181cea9963587d5ec5ef3bf2f21cf86f847d47..44a864fad066dec9557c3812c503e3de86fd2a15 100755 (executable)
@@ -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() {