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() {