repart: add EncryptAddToken= to register custom LUKS2 tokens at provisioning time

Adds EncryptAddToken=<type>[:<source>:<value>] to repart.d partition
files, registering one or more LUKS2 token objects in the partition
header after the keyslot is created. This allows custom token plugins
(e.g. a TDX KBS plugin) to be wired up at provisioning time without a
separate cryptsetup token import step.

The type name is always explicit and extra token fields can be supplied as
a JSON file (:file:/path) or base64-encoded JSON (:data:<base64>). The
type and keyslots fields are always injected by repart. The option is
repeatable; an empty value clears all tokens for the partition.

Supported with Encrypt=key-file, tpm2, and key-file+tpm2.
This commit is contained in:
Nandakumar Raghavan
2026-09-22 21:26:20 +02:00
committed by Luca Boccassi
parent 06c2dfa73c
commit c99af7968e
3 changed files with 449 additions and 1 deletions
+53
View File
@@ -773,6 +773,59 @@
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>EncryptAddToken=</varname></term>
<listitem><para>Registers a custom LUKS2 token in the partition's LUKS2 header at provisioning
time. This option may be specified more than once to register multiple tokens. Each invocation adds
one token entry. Assigning the empty string clears all previously assigned tokens.</para>
<para>The syntax is:</para>
<programlisting>EncryptAddToken=<replaceable>type</replaceable>[:<replaceable>source</replaceable>:<replaceable>value</replaceable>]</programlisting>
<para>The <replaceable>type</replaceable> field is mandatory and specifies the LUKS2 token type
name (e.g. <literal>tdx-kbs</literal>). It must be a valid filename component (no
<literal>/</literal>, no <literal>.</literal> or <literal>..</literal>) and corresponds to the
name of the token handler plugin installed as
<filename>libcryptsetup-token-<replaceable>type</replaceable>.so</filename>.</para>
<para>The optional <replaceable>source</replaceable> and <replaceable>value</replaceable> fields
supply token-plugin-specific metadata as a JSON object. Two sources are supported:</para>
<variablelist>
<varlistentry>
<term><literal>file</literal></term>
<listitem><para>The <replaceable>value</replaceable> is a path to a file containing a JSON
object with the extra token metadata. Example:</para>
<programlisting>EncryptAddToken=tdx-kbs:file:/etc/repart/tdx-kbs-token.json</programlisting>
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
</varlistentry>
<varlistentry>
<term><literal>data</literal></term>
<listitem><para>The <replaceable>value</replaceable> is a Base64-encoded JSON object with the
extra token metadata, suitable for embedding directly in the configuration file. Example:</para>
<programlisting>EncryptAddToken=tdx-kbs:data:eyJ0cnVzdGVlLmticy51cmwiOiJodHRwOi8va2JzLXNlcnZpY2U6ODA4MCJ9</programlisting>
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
</varlistentry>
</variablelist>
<para>The extra metadata object must not contain <literal>type</literal> or
<literal>keyslots</literal> fields — these are always set by
<command>systemd-repart</command>: <literal>type</literal> is taken from the
<replaceable>type</replaceable> argument, and <literal>keyslots</literal> is set to the keyslot
number created for this partition.</para>
<para>This option is supported with all <varname>Encrypt=</varname> modes except
<literal>off</literal>. When used with <literal>key-file+tpm2</literal>, the token is bound to the
key-file keyslot. When used with <literal>tpm2</literal>, the token is bound to the TPM2
keyslot.</para>
<para>This option has no effect if the partition already exists.</para>
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>Verity=</varname></term>
+210 -1
View File
@@ -350,6 +350,11 @@ typedef struct PartitionEncryptedVolume {
bool fixate_volume_key;
} PartitionEncryptedVolume;
typedef struct PartitionEncryptAddToken {
char *type; /* token type name, always set */
sd_json_variant *data; /* extra fields (excluding type/keyslots), may be NULL */
} PartitionEncryptAddToken;
static PartitionEncryptedVolume* partition_encrypted_volume_free(PartitionEncryptedVolume *c) {
if (!c)
return NULL;
@@ -361,6 +366,19 @@ static PartitionEncryptedVolume* partition_encrypted_volume_free(PartitionEncryp
return mfree(c);
}
static void partition_encrypt_add_token_done(PartitionEncryptAddToken *t) {
assert(t);
free(t->type);
sd_json_variant_unref(t->data);
}
static void partition_encrypt_add_token_free_many(PartitionEncryptAddToken *tokens, size_t n) {
assert(tokens || n == 0);
FOREACH_ARRAY(t, tokens, n)
partition_encrypt_add_token_done(t);
free(tokens);
}
typedef struct CopyFiles {
char *source;
char *target;
@@ -545,6 +563,8 @@ typedef struct Partition {
size_t n_mountpoints;
PartitionEncryptedVolume *encrypted_volume;
PartitionEncryptAddToken *encrypt_add_tokens;
size_t n_encrypt_add_tokens;
unsigned last_percent;
RateLimit progress_ratelimit;
@@ -873,6 +893,7 @@ static Partition* partition_free(Partition *p) {
p->n_mountpoints = 0;
partition_encrypted_volume_free(p->encrypted_volume);
partition_encrypt_add_token_free_many(p->encrypt_add_tokens, p->n_encrypt_add_tokens);
partition_unlink_supplement(p);
@@ -933,6 +954,9 @@ static void partition_foreignize(Partition *p) {
p->n_mountpoints = 0;
p->encrypted_volume = partition_encrypted_volume_free(p->encrypted_volume);
partition_encrypt_add_token_free_many(p->encrypt_add_tokens, p->n_encrypt_add_tokens);
p->encrypt_add_tokens = NULL;
p->n_encrypt_add_tokens = 0;
partition_unlink_supplement(p);
}
@@ -2694,7 +2718,7 @@ static int config_parse_encrypted_volume(
int r;
if (isempty(rvalue)) {
p->encrypted_volume = mfree(p->encrypted_volume);
p->encrypted_volume = partition_encrypted_volume_free(p->encrypted_volume);
return 0;
}
@@ -2761,6 +2785,139 @@ static int config_parse_encrypted_volume(
return 0;
}
static int config_parse_encrypt_add_token(
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 *p = ASSERT_PTR(data);
int r;
assert(rvalue);
if (isempty(rvalue)) {
partition_encrypt_add_token_free_many(p->encrypt_add_tokens, p->n_encrypt_add_tokens);
p->encrypt_add_tokens = NULL;
p->n_encrypt_add_tokens = 0;
return 0;
}
/* Syntax: EncryptAddToken=<type>[:<source>:<value>]
* where source is "file" (path) or "data" (base64-encoded JSON).
* The token type name is always supplied explicitly and stored separately.
* Extra metadata may be supplied as:
* :file:/path/to/metadata.json — read JSON from file
* :data:<base64> — inline base64-encoded JSON
*/
_cleanup_free_ char *type = NULL, *source = NULL;
const char *q = rvalue;
/* Extract only type and source; leave the rest of q as the value verbatim.
* This allows ':' in the value (e.g. file paths containing colons). */
r = extract_many_words(&q, ":", EXTRACT_DONT_COALESCE_SEPARATORS, &type, &source);
if (r == -ENOMEM)
return log_oom();
if (r < 0)
return log_syntax(unit, LOG_ERR, filename, line, r,
"Failed to parse %s=: %s", lvalue, rvalue);
if (r < 1)
return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
"%s= requires at least a token type name.", lvalue);
/* Validate the type name: it becomes the suffix of libcryptsetup-token-<type>.so, so it must
* be a valid filename component — no slashes, no '.' or '..' path traversal. */
if (isempty(type) || dot_or_dot_dot(type) || !filename_part_is_valid(type))
return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
"Invalid token type name in %s= (must be a valid filename component): %s", lvalue, type);
/* Refuse the token type names systemd own cryptsetup token plugins use */
if (STR_IN_SET(type, "systemd-tpm2", "systemd-fido2", "systemd-pkcs11", "systemd-recovery"))
return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
"Token type name in %s= is reserved for systemd's own use: %s", lvalue, type);
_cleanup_(sd_json_variant_unrefp) sd_json_variant *extra = NULL;
if (source) {
/* EXTRACT_DONT_COALESCE_SEPARATORS yields an empty-string source for inputs like
* 'type:' (trailing colon) or 'type::value' (double colon) — reject explicitly. */
if (isempty(source))
return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
"Empty source in %s=, expected 'file' or 'data': %s", lvalue, rvalue);
/* Value is the remainder of the input after source, so ':' in it is preserved. */
if (isempty(q))
return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
"%s= source '%s' requires a value.", lvalue, source);
_cleanup_free_ char *json_text = NULL;
if (streq(source, "file")) {
if (!path_is_absolute(q))
return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
"Path in %s= is not absolute: %s", lvalue, q);
/* Load JSON from a file; cap at 64K — token metadata is tiny */
r = read_full_file_full(
AT_FDCWD, q,
/* offset= */ UINT64_MAX,
/* size= */ 64 * 1024,
READ_FULL_FILE_FAIL_WHEN_LARGER | READ_FULL_FILE_VERIFY_REGULAR,
/* bind_name= */ NULL,
&json_text, /* ret_size= */ NULL);
if (r < 0)
return log_syntax(unit, LOG_ERR, filename, line, r,
"Failed to read token data file '%s': %m", q);
} else if (streq(source, "data")) {
/* Decode base64-encoded JSON */
_cleanup_free_ void *decoded = NULL;
size_t decoded_size = 0;
r = unbase64mem(q, &decoded, &decoded_size);
if (r < 0)
return log_syntax(unit, LOG_ERR, filename, line, r,
"Failed to decode base64 token data in %s=: %m", lvalue);
/* JSON cannot contain raw NUL bytes; reject early rather than silently truncating */
if (memchr(decoded, '\0', decoded_size))
return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
"Base64-decoded token data in %s= contains embedded NUL bytes.", lvalue);
json_text = strndup(decoded, decoded_size);
if (!json_text)
return log_oom();
} else
return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
"Unknown source '%s' in %s=, expected 'file' or 'data'.", source, lvalue);
r = sd_json_parse(json_text, SD_JSON_PARSE_MUST_BE_OBJECT, &extra, NULL, NULL);
if (r < 0)
return log_syntax(unit, LOG_ERR, filename, line, r,
"Token data in %s= is not a valid JSON object: %m", lvalue);
/* Reject any pre-set "type" or "keyslots" — we manage those */
if (sd_json_variant_by_key(extra, "type"))
return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
"Token data in %s= must not contain a \"type\" field (set by the type name argument).", lvalue);
if (sd_json_variant_by_key(extra, "keyslots"))
return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
"Token data in %s= must not contain a \"keyslots\" field (set automatically).", lvalue);
}
if (!GREEDY_REALLOC(p->encrypt_add_tokens, p->n_encrypt_add_tokens + 1))
return log_oom();
p->encrypt_add_tokens[p->n_encrypt_add_tokens++] = (PartitionEncryptAddToken) {
.type = TAKE_PTR(type),
.data = TAKE_PTR(extra),
};
return 0;
}
static int config_parse_tpm2_pcrs(
const char *unit,
const char *filename,
@@ -2995,6 +3152,7 @@ static int partition_read_definition(
{ "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", "EncryptAddToken", config_parse_encrypt_add_token, 0, p },
{ "Partition", "TPM2PCRs", config_parse_tpm2_pcrs, 0, p },
{ "Partition", "KeyFile", config_parse_key_file, 0, p },
{ "Partition", "Integrity", config_parse_integrity, 0, &p->integrity },
@@ -3208,6 +3366,10 @@ static int partition_read_definition(
log_syntax(NULL, LOG_WARNING, path, 1, 0,
"EncryptKDF= has no effect with Encrypt=off.");
if (p->n_encrypt_add_tokens > 0 && p->encrypt == ENCRYPT_OFF)
log_syntax(NULL, LOG_WARNING, path, 1, 0,
"EncryptAddToken= has no effect with Encrypt=off.");
if (p->encrypt != ENCRYPT_OFF && p->integrity == INTEGRITY_INLINE && p->discard > 0)
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
"Integrity=inline is incompatible with Discard=yes.");
@@ -5475,6 +5637,39 @@ static size_t dmcrypt_proper_key_size(Partition *p) {
}
}
#if HAVE_LIBCRYPTSETUP
static int partition_add_tokens_for_keyslot(Partition *p, struct crypt_device *cd, int keyslot) {
int r;
assert(p);
assert(cd);
char keyslot_str[DECIMAL_STR_MAX(int)];
xsprintf(keyslot_str, "%i", keyslot);
FOREACH_ARRAY(t, p->encrypt_add_tokens, p->n_encrypt_add_tokens) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
/* Start with extra data if any (NULL-safe), then inject type and keyslots.
* merge_objectbo handles a blank/NULL *v by replacing it with the merged object,
* so this works whether or not t->data is set. */
v = sd_json_variant_ref(t->data);
r = sd_json_variant_merge_objectbo(&v,
SD_JSON_BUILD_PAIR_STRING("type", t->type),
SD_JSON_BUILD_PAIR("keyslots",
SD_JSON_BUILD_ARRAY(SD_JSON_BUILD_STRING(keyslot_str))));
if (r < 0)
return log_error_errno(r, "Failed to build LUKS2 token JSON for type '%s': %m", t->type);
r = cryptsetup_add_token_json(cd, v);
if (r < 0)
return log_error_errno(r, "Failed to add LUKS2 token '%s' to header: %m", t->type);
}
return 0;
}
#endif
static int partition_encrypt(Context *context, Partition *p, PartitionTarget *target, bool offline, bool temporary) {
#if HAVE_LIBCRYPTSETUP
#if HAVE_TPM2
@@ -5679,8 +5874,14 @@ static int partition_encrypt(Context *context, Partition *p, PartitionTarget *ta
if (r < 0)
return log_error_errno(r, "Failed to add LUKS2 key: %m");
int key_file_keyslot = r;
passphrase = strempty(iovec_key->iov_base);
passphrase_size = iovec_key->iov_len;
r = partition_add_tokens_for_keyslot(p, cd, key_file_keyslot);
if (r < 0)
return r;
}
if (IN_SET(p->encrypt, ENCRYPT_TPM2, ENCRYPT_KEY_FILE_TPM2)) {
@@ -5886,6 +6087,14 @@ static int partition_encrypt(Context *context, Partition *p, PartitionTarget *ta
if (r < 0)
return log_error_errno(r, "Failed to add TPM2 JSON token to LUKS2 header: %m");
/* For key-file+tpm2 the custom tokens were already bound to the key-file keyslot
* above; only bind them to the TPM2 keyslot in pure TPM2 mode. */
if (p->encrypt == ENCRYPT_TPM2) {
r = partition_add_tokens_for_keyslot(p, cd, keyslot);
if (r < 0)
return r;
}
passphrase = base64_encoded;
passphrase_size = strlen(base64_encoded);
#else
+186
View File
@@ -2940,6 +2940,192 @@ EOF
losetup -d "$loop"
}
testcase_encrypt_add_token() {
local defs imgs loop token_json metadata_json metadata_b64
defs="$(mktemp --directory "/tmp/test-repart.defs.XXXXXXXXXX")"
imgs="$(mktemp --directory "/var/tmp/test-repart.imgs.XXXXXXXXXX")"
# shellcheck disable=SC2064
trap "rm -rf '$defs' '$imgs'" RETURN
chmod 0755 "$defs"
echo "*** testcase for EncryptAddToken= (plain type name, no extra data) ***"
tee "$defs/root.conf" <<EOF
[Partition]
Type=linux-generic
Format=ext4
Encrypt=key-file
EncryptAddToken=test-dummy-token
EOF
systemd-repart --pretty=yes \
--definitions "$defs" \
--empty=create \
--size=100M \
--seed="$seed" \
--dry-run=no \
--offline=no \
"$imgs/enctoken-plain.img"
loop="$(losetup -P --show --find "$imgs/enctoken-plain.img")"
# shellcheck disable=SC2064
trap "rm -rf '$defs' '$imgs'; losetup -d '$loop'" RETURN
udevadm wait --timeout="$UDEVADM_WAIT_TIMEOUT" --settle "${loop:?}p1"
cryptsetup luksDump "${loop}p1" | grep "test-dummy-token" >/dev/null
token_json="$(cryptsetup token export --token-id 0 "${loop}p1")"
echo "$token_json" | python3 -c "
import json, sys
d = json.load(sys.stdin)
assert d['type'] == 'test-dummy-token', 'Wrong token type: {}'.format(d['type'])
assert '0' in d.get('keyslots', []), 'keyslot 0 not referenced by token: {}'.format(d)
"
losetup -d "$loop"
# shellcheck disable=SC2064
trap "rm -rf '$defs' '$imgs'" RETURN
echo "*** testcase for EncryptAddToken= (type + extra data via :data: base64) ***"
rm -f "$defs/root.conf"
# Build base64-encoded JSON for the extra metadata (no type/keyslots)
metadata_json='{"trustee.kbs.url":"http://kbs-service:8080"}'
metadata_b64="$(echo -n "$metadata_json" | base64 -w0)"
tee "$defs/root.conf" <<EOF
[Partition]
Type=linux-generic
Format=ext4
Encrypt=key-file
EncryptAddToken=test-dummy-token:data:${metadata_b64}
EOF
systemd-repart --pretty=yes \
--definitions "$defs" \
--empty=create \
--size=100M \
--seed="$seed" \
--dry-run=no \
--offline=no \
"$imgs/enctoken-data.img"
loop="$(losetup -P --show --find "$imgs/enctoken-data.img")"
# shellcheck disable=SC2064
trap "rm -rf '$defs' '$imgs'; losetup -d '$loop'" RETURN
udevadm wait --timeout="$UDEVADM_WAIT_TIMEOUT" --settle "${loop:?}p1"
cryptsetup luksDump "${loop}p1" | grep "test-dummy-token" >/dev/null
token_json="$(cryptsetup token export --token-id 0 "${loop}p1")"
echo "$token_json" | python3 -c "
import json, sys
d = json.load(sys.stdin)
assert d['type'] == 'test-dummy-token', 'Wrong token type: {}'.format(d['type'])
assert d.get('trustee.kbs.url') == 'http://kbs-service:8080', 'Extra field not preserved: {}'.format(d)
assert '0' in d.get('keyslots', []), 'keyslot 0 not referenced by token: {}'.format(d)
"
losetup -d "$loop"
# shellcheck disable=SC2064
trap "rm -rf '$defs' '$imgs'" RETURN
echo "*** testcase for EncryptAddToken= (type + extra data via :file:) ***"
rm -f "$defs/root.conf"
echo -n '{"trustee.kbs.url":"http://kbs-service:9090"}' > "$imgs/token-meta.json"
tee "$defs/root.conf" <<EOF
[Partition]
Type=linux-generic
Format=ext4
Encrypt=key-file
EncryptAddToken=test-dummy-token:file:${imgs}/token-meta.json
EOF
systemd-repart --pretty=yes \
--definitions "$defs" \
--empty=create \
--size=100M \
--seed="$seed" \
--dry-run=no \
--offline=no \
"$imgs/enctoken-file.img"
loop="$(losetup -P --show --find "$imgs/enctoken-file.img")"
# shellcheck disable=SC2064
trap "rm -rf '$defs' '$imgs'; losetup -d '$loop'" RETURN
udevadm wait --timeout="$UDEVADM_WAIT_TIMEOUT" --settle "${loop:?}p1"
token_json="$(cryptsetup token export --token-id 0 "${loop}p1")"
echo "$token_json" | python3 -c "
import json, sys
d = json.load(sys.stdin)
assert d['type'] == 'test-dummy-token', 'Wrong token type: {}'.format(d['type'])
assert d.get('trustee.kbs.url') == 'http://kbs-service:9090', 'Extra field from file not preserved: {}'.format(d)
assert '0' in d.get('keyslots', []), 'keyslot 0 not referenced by token: {}'.format(d)
"
losetup -d "$loop"
# shellcheck disable=SC2064
trap "rm -rf '$defs' '$imgs'" RETURN
echo "*** testcase for EncryptAddToken= (multiple tokens on one partition) ***"
rm -f "$defs/root.conf"
metadata_b64="$(echo -n '{"extra-key":"extra-val"}' | base64 -w0)"
tee "$defs/root.conf" <<EOF
[Partition]
Type=linux-generic
Format=ext4
Encrypt=key-file
EncryptAddToken=first-token
EncryptAddToken=second-token:data:${metadata_b64}
EOF
systemd-repart --pretty=yes \
--definitions "$defs" \
--empty=create \
--size=100M \
--seed="$seed" \
--dry-run=no \
--offline=no \
"$imgs/enctoken-multi.img"
loop="$(losetup -P --show --find "$imgs/enctoken-multi.img")"
# shellcheck disable=SC2064
trap "rm -rf '$defs' '$imgs'; losetup -d '$loop'" RETURN
udevadm wait --timeout="$UDEVADM_WAIT_TIMEOUT" --settle "${loop:?}p1"
# Both tokens should appear in luksDump
cryptsetup luksDump "${loop}p1" | grep "first-token" >/dev/null
cryptsetup luksDump "${loop}p1" | grep "second-token" >/dev/null
# Verify each token individually
cryptsetup token export --token-id 0 "${loop}p1" | python3 -c "
import json, sys
d = json.load(sys.stdin)
assert d['type'] == 'first-token', 'Wrong first token type: {}'.format(d['type'])
assert '0' in d.get('keyslots', []), 'keyslot not in first token: {}'.format(d)
"
cryptsetup token export --token-id 1 "${loop}p1" | python3 -c "
import json, sys
d = json.load(sys.stdin)
assert d['type'] == 'second-token', 'Wrong second token type: {}'.format(d['type'])
assert d.get('extra-key') == 'extra-val', 'Extra field missing in second token: {}'.format(d)
assert '0' in d.get('keyslots', []), 'keyslot not in second token: {}'.format(d)
"
losetup -d "$loop"
# shellcheck disable=SC2064
trap "rm -rf '$defs' '$imgs'" RETURN
}
testcase_fstab_crypttab_in_repart() {
local defs imgs root volume