<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>
<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>
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);
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;
#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;
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)
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)
LIST_FOREACH(partitions, p, context->partitions) {
_cleanup_(partition_target_freep) PartitionTarget *t = NULL;
- if (p->copy_blocks_fd < 0)
- continue;
-
if (p->dropped)
continue;
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;
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);
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;
" 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"
ARG_GENERATE_FSTAB,
ARG_GENERATE_CRYPTTAB,
ARG_LIST_DEVICES,
+ ARG_JOIN_SIGNATURE,
};
static const struct option options[] = {
{ "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 },
{}
};
return 0;
+ case ARG_JOIN_SIGNATURE:
+ r = parse_join_signature(optarg, &arg_verity_settings);
+ if (r < 0)
+ return r;
+ break;
+
case '?':
return -EINVAL;
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.");