systemd-repart: add encryption configs into repart.d/* (#38052)

As explained in https://github.com/systemd/systemd/issues/37892, it
would be nice to define per-partition PCRs/key file to use.

The global default config will be still defined as cmdline options, and
`TPM2PCRs=` and `KeyFile=` will be overriden by them.
This commit is contained in:
Yu Watanabe
2025-09-18 11:02:38 +09:00
committed by GitHub
3 changed files with 136 additions and 35 deletions

View File

@@ -869,6 +869,31 @@
<xi:include href="version-info.xml" xpointer="v256"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>TPM2PCRs=</varname></term>
<listitem><para>Configures the list of PCRs to use for LUKS2 volumes configured with
the <varname>Encrypt=tpm2</varname> setting in partition files.
This option take the same parameters as the similary named options to
<citerefentry><refentrytitle>systemd-cryptenroll</refentrytitle><manvolnum>1</manvolnum></citerefentry>
and have the same effect on partitions where TPM2 enrollment is requested.
This option will be overridden by the global <varname>--tpm2-pcrs=</varname> option.</para>
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>KeyFile=</varname></term>
<listitem><para>Takes a file system path. This path must be absolute, otherwise the option is ignored.
Configures the encryption key to use when setting up LUKS2 volumes configured with the
<varname>Encrypt=key-file</varname> setting in partition files. Please refer to the documentation of
<varname>--key-file=</varname> for more details. This option will be overridden by the global
<varname>--key-file=</varname> option.</para>
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>Compression=</varname></term>

View File

@@ -338,9 +338,9 @@
volumes configured with the <varname>Encrypt=key-file</varname> setting in partition files. Should
refer to a regular file containing the key, or an <constant>AF_UNIX</constant> stream socket in the
file system. In the latter case, a connection is made to it and the key read from it. If this switch
is not specified, the empty key (i.e. zero length key) is used. This behaviour is useful for setting
up encrypted partitions during early first boot that receive their user-supplied password only in a
later setup step.</para>
is not specified, and no <varname>KeyFile=</varname> is specified in the partition file, the empty
key (i.e. zero length key) is used. This behaviour is useful for setting up encrypted partitions during
early first boot that receive their user-supplied password only in a later setup step.</para>
<xi:include href="version-info.xml" xpointer="v247"/></listitem>
</varlistentry>

View File

@@ -169,8 +169,7 @@ static bool arg_size_auto = false;
static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
static PagerFlags arg_pager_flags = 0;
static bool arg_legend = true;
static void *arg_key = NULL;
static size_t arg_key_size = 0;
static struct iovec arg_key = {};
static char *arg_private_key = NULL;
static KeySourceType arg_private_key_source_type = OPENSSL_KEY_SOURCE_FILE;
static char *arg_private_key_source = NULL;
@@ -208,7 +207,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_node, freep);
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
STATIC_DESTRUCTOR_REGISTER(arg_definitions, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_key, erase_and_freep);
STATIC_DESTRUCTOR_REGISTER(arg_key, iovec_done_erase);
STATIC_DESTRUCTOR_REGISTER(arg_private_key, freep);
STATIC_DESTRUCTOR_REGISTER(arg_private_key_source, freep);
STATIC_DESTRUCTOR_REGISTER(arg_certificate, freep);
@@ -413,6 +412,9 @@ typedef struct Partition {
OrderedHashmap *subvolumes;
char *default_subvolume;
EncryptMode encrypt;
struct iovec key;
Tpm2PCRValue *tpm2_hash_pcr_values;
size_t tpm2_n_hash_pcr_values;
VerityMode verity;
char *verity_match_key;
MinimizeMode minimize;
@@ -676,10 +678,13 @@ static Partition* partition_free(Partition *p) {
strv_free(p->make_symlinks);
ordered_hashmap_free(p->subvolumes);
free(p->default_subvolume);
free(p->tpm2_hash_pcr_values);
free(p->verity_match_key);
free(p->compression);
free(p->compression_level);
iovec_done_erase(&p->key);
copy_files_free_many(p->copy_files, p->n_copy_files);
iovec_done(&p->roothash);
@@ -717,10 +722,13 @@ static void partition_foreignize(Partition *p) {
p->make_symlinks = strv_free(p->make_symlinks);
p->subvolumes = ordered_hashmap_free(p->subvolumes);
p->default_subvolume = mfree(p->default_subvolume);
p->tpm2_hash_pcr_values = mfree(p->tpm2_hash_pcr_values);
p->verity_match_key = mfree(p->verity_match_key);
p->compression = mfree(p->compression);
p->compression_level = mfree(p->compression_level);
iovec_done_erase(&p->key);
copy_files_free_many(p->copy_files, p->n_copy_files);
p->copy_files = NULL;
p->n_copy_files = 0;
@@ -2467,6 +2475,78 @@ static int config_parse_encrypted_volume(
return 0;
}
static int config_parse_tpm2_pcrs(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
Partition *partition = ASSERT_PTR(data);
assert(rvalue);
if (isempty(rvalue)) {
/* Clear existing PCR values if empty */
partition->tpm2_hash_pcr_values = mfree(partition->tpm2_hash_pcr_values);
partition->tpm2_n_hash_pcr_values = 0;
return 0;
}
return tpm2_parse_pcr_argument_append(rvalue, &partition->tpm2_hash_pcr_values,
&partition->tpm2_n_hash_pcr_values);
}
static int parse_key_file(const char *filename, struct iovec *key) {
_cleanup_(erase_and_freep) char *k = NULL;
size_t n = 0;
int r;
r = read_full_file_full(
AT_FDCWD, filename,
/* offset= */ UINT64_MAX,
/* size= */ SIZE_MAX,
READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET,
/* bind_name= */ NULL,
&k, &n);
if (r < 0)
return log_error_errno(r, "Failed to read key file '%s': %m", filename);
iovec_done_erase(key);
*key = IOVEC_MAKE(TAKE_PTR(k), n);
return 0;
}
static int config_parse_key_file(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
Partition *partition = ASSERT_PTR(userdata);
assert(rvalue);
if (isempty(rvalue)) {
iovec_done_erase(&partition->key);
return 0;
}
return parse_key_file(rvalue, &partition->key);
}
static DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT(config_parse_verity, verity_mode, VerityMode, VERITY_OFF);
static DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT(config_parse_minimize, minimize_mode, MinimizeMode, MINIMIZE_OFF);
@@ -2572,6 +2652,8 @@ static int partition_read_definition(Partition *p, const char *path, const char
{ "Partition", "VerityHashBlockSizeBytes", config_parse_block_size, 0, &p->verity_hash_block_size },
{ "Partition", "MountPoint", config_parse_mountpoint, 0, p },
{ "Partition", "EncryptedVolume", config_parse_encrypted_volume, 0, p },
{ "Partition", "TPM2PCRs", config_parse_tpm2_pcrs, 0, p },
{ "Partition", "KeyFile", config_parse_key_file, 0, p },
{ "Partition", "Compression", config_parse_string, CONFIG_PARSE_STRING_SAFE_AND_ASCII, &p->compression },
{ "Partition", "CompressionLevel", config_parse_string, CONFIG_PARSE_STRING_SAFE_AND_ASCII, &p->compression_level },
{ "Partition", "SupplementFor", config_parse_string, 0, &p->supplement_for_name },
@@ -4793,18 +4875,21 @@ static int partition_encrypt(Context *context, Partition *p, PartitionTarget *ta
return log_error_errno(r, "Failed to LUKS2 format future partition: %m");
if (IN_SET(p->encrypt, ENCRYPT_KEY_FILE, ENCRYPT_KEY_FILE_TPM2)) {
/* Use partition-specific key if available, otherwise fall back to global key */
struct iovec *iovec_key = arg_key.iov_base ? &arg_key : &p->key;
r = sym_crypt_keyslot_add_by_volume_key(
cd,
CRYPT_ANY_SLOT,
NULL,
VOLUME_KEY_SIZE,
strempty(arg_key),
arg_key_size);
strempty(iovec_key->iov_base),
iovec_key->iov_len);
if (r < 0)
return log_error_errno(r, "Failed to add LUKS2 key: %m");
passphrase = strempty(arg_key);
passphrase_size = arg_key_size;
passphrase = strempty(iovec_key->iov_base);
passphrase_size = iovec_key->iov_len;
}
if (IN_SET(p->encrypt, ENCRYPT_TPM2, ENCRYPT_KEY_FILE_TPM2)) {
@@ -4815,8 +4900,10 @@ static int partition_encrypt(Context *context, Partition *p, PartitionTarget *ta
ssize_t base64_encoded_size;
int keyslot;
TPM2Flags flags = 0;
Tpm2PCRValue *pcr_values = arg_tpm2_n_hash_pcr_values > 0 ? arg_tpm2_hash_pcr_values : p->tpm2_hash_pcr_values;
size_t n_pcr_values = arg_tpm2_n_hash_pcr_values > 0 ? arg_tpm2_n_hash_pcr_values : p->tpm2_n_hash_pcr_values;
if (arg_tpm2_n_hash_pcr_values == 0 &&
if (n_pcr_values == 0 &&
arg_tpm2_public_key_pcr_mask == 0 &&
!arg_tpm2_pcrlock)
log_notice("Notice: encrypting future partition %" PRIu64 ", locking against TPM2 with an empty policy, i.e. without any state or access restrictions.\n"
@@ -4856,7 +4943,7 @@ static int partition_encrypt(Context *context, Partition *p, PartitionTarget *ta
if (r < 0)
return r;
if (!tpm2_pcr_values_has_all_values(arg_tpm2_hash_pcr_values, arg_tpm2_n_hash_pcr_values))
if (!tpm2_pcr_values_has_all_values(pcr_values, n_pcr_values))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Must provide all PCR values when using TPM2 device key.");
} else {
@@ -4864,8 +4951,8 @@ static int partition_encrypt(Context *context, Partition *p, PartitionTarget *ta
if (r < 0)
return r;
if (!tpm2_pcr_values_has_all_values(arg_tpm2_hash_pcr_values, arg_tpm2_n_hash_pcr_values)) {
r = tpm2_pcr_read_missing_values(tpm2_context, arg_tpm2_hash_pcr_values, arg_tpm2_n_hash_pcr_values);
if (!tpm2_pcr_values_has_all_values(pcr_values, n_pcr_values)) {
r = tpm2_pcr_read_missing_values(tpm2_context, pcr_values, n_pcr_values);
if (r < 0)
return log_error_errno(r, "Could not read pcr values: %m");
}
@@ -4873,17 +4960,17 @@ static int partition_encrypt(Context *context, Partition *p, PartitionTarget *ta
uint16_t hash_pcr_bank = 0;
uint32_t hash_pcr_mask = 0;
if (arg_tpm2_n_hash_pcr_values > 0) {
if (n_pcr_values > 0) {
size_t hash_count;
r = tpm2_pcr_values_hash_count(arg_tpm2_hash_pcr_values, arg_tpm2_n_hash_pcr_values, &hash_count);
r = tpm2_pcr_values_hash_count(pcr_values, n_pcr_values, &hash_count);
if (r < 0)
return log_error_errno(r, "Could not get hash count: %m");
if (hash_count > 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Multiple PCR banks selected.");
hash_pcr_bank = arg_tpm2_hash_pcr_values[0].hash;
r = tpm2_pcr_values_to_mask(arg_tpm2_hash_pcr_values, arg_tpm2_n_hash_pcr_values, hash_pcr_bank, &hash_pcr_mask);
hash_pcr_bank = pcr_values[0].hash;
r = tpm2_pcr_values_to_mask(pcr_values, n_pcr_values, hash_pcr_bank, &hash_pcr_mask);
if (r < 0)
return log_error_errno(r, "Could not get hash mask: %m");
}
@@ -4896,8 +4983,8 @@ static int partition_encrypt(Context *context, Partition *p, PartitionTarget *ta
/* If both PCR public key unlock and pcrlock unlock is selected, then shard the encryption key. */
r = tpm2_calculate_sealing_policy(
arg_tpm2_hash_pcr_values,
arg_tpm2_n_hash_pcr_values,
pcr_values,
n_pcr_values,
iovec_is_set(&pubkey) ? &public : NULL,
/* use_pin= */ false,
arg_tpm2_pcrlock && !iovec_is_set(&pubkey) ? &pcrlock_policy : NULL,
@@ -4907,8 +4994,8 @@ static int partition_encrypt(Context *context, Partition *p, PartitionTarget *ta
if (arg_tpm2_pcrlock && iovec_is_set(&pubkey)) {
r = tpm2_calculate_sealing_policy(
arg_tpm2_hash_pcr_values,
arg_tpm2_n_hash_pcr_values,
pcr_values,
n_pcr_values,
/* public= */ NULL, /* Turn this one off for the 2nd shard */
/* use_pin= */ false,
&pcrlock_policy, /* But turn this one on */
@@ -8829,20 +8916,9 @@ static int parse_argv(int argc, char *argv[], X509 **ret_certificate, EVP_PKEY *
break;
case ARG_KEY_FILE: {
_cleanup_(erase_and_freep) char *k = NULL;
size_t n = 0;
r = read_full_file_full(
AT_FDCWD, optarg, UINT64_MAX, SIZE_MAX,
READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET,
NULL,
&k, &n);
r = parse_key_file(optarg, &arg_key);
if (r < 0)
return log_error_errno(r, "Failed to read key file '%s': %m", optarg);
erase_and_free(arg_key);
arg_key = TAKE_PTR(k);
arg_key_size = n;
return r;
break;
}