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:
committed by
Luca Boccassi
parent
06c2dfa73c
commit
c99af7968e
+210
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user