diff --git a/man/repart.d.xml b/man/repart.d.xml
index 3e012f8619..7120a466a2 100644
--- a/man/repart.d.xml
+++ b/man/repart.d.xml
@@ -773,6 +773,59 @@
+
+ EncryptAddToken=
+
+ 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.
+
+ The syntax is:
+ EncryptAddToken=type[:source:value]
+
+ The type field is mandatory and specifies the LUKS2 token type
+ name (e.g. tdx-kbs). It must be a valid filename component (no
+ /, no . or ..) and corresponds to the
+ name of the token handler plugin installed as
+ libcryptsetup-token-type.so.
+
+ The optional source and value fields
+ supply token-plugin-specific metadata as a JSON object. Two sources are supported:
+
+
+
+ file
+ The value is a path to a file containing a JSON
+ object with the extra token metadata. Example:
+ EncryptAddToken=tdx-kbs:file:/etc/repart/tdx-kbs-token.json
+
+
+
+
+ data
+ The value is a Base64-encoded JSON object with the
+ extra token metadata, suitable for embedding directly in the configuration file. Example:
+ EncryptAddToken=tdx-kbs:data:eyJ0cnVzdGVlLmticy51cmwiOiJodHRwOi8va2JzLXNlcnZpY2U6ODA4MCJ9
+
+
+
+
+ The extra metadata object must not contain type or
+ keyslots fields — these are always set by
+ systemd-repart: type is taken from the
+ type argument, and keyslots is set to the keyslot
+ number created for this partition.
+
+ This option is supported with all Encrypt= modes except
+ off. When used with key-file+tpm2, the token is bound to the
+ key-file keyslot. When used with tpm2, the token is bound to the TPM2
+ keyslot.
+
+ This option has no effect if the partition already exists.
+
+
+
+
Verity=
diff --git a/src/repart/repart.c b/src/repart/repart.c
index 388f25c398..d042b3255f 100644
--- a/src/repart/repart.c
+++ b/src/repart/repart.c
@@ -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=[::]
+ * 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: — 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-.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
diff --git a/test/units/TEST-58-REPART.sh b/test/units/TEST-58-REPART.sh
index c862c07d2e..05b1090cc0 100755
--- a/test/units/TEST-58-REPART.sh
+++ b/test/units/TEST-58-REPART.sh
@@ -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" </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" </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" </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