## Summary Closes #18280. `systemd-homed` currently writes fscrypt v1 policies, which bind the master key to the calling process's keyring. This means files in a homed-managed directory aren't readable when accessed through a container bind mount, a different mount namespace, or by any process other than the one that first unlocked the home. Reading the file from a context that has the key first warms the page cache, but the underlying problem remains. v2 policies (Linux 5.4+) route the master key through the filesystem keyring, via `FS_IOC_ADD_ENCRYPTION_KEY` / `FS_IOC_REMOVE_ENCRYPTION_KEY`, so the key is visible to every process accessing the filesystem. This PR switches `systemd-homed` to v2 by default and keeps existing v1 homes working. ## Changes **`shared/crypto-util`**: Add `kdf_hkdf_derive` (HKDF, RFC 5869) wrapper around OpenSSL's `EVP_KDF` "HKDF", alongside the existing SSKDF/KBKDF helpers. **`homed/fscrypt`**: - Read the existing policy via `FS_IOC_GET_ENCRYPTION_POLICY_EX`, falling back to the legacy ioctl on pre-5.4 kernels. - `HomeSetup` now carries a full `struct fscrypt_key_specifier` instead of a bare 8-byte descriptor. Slot decryption derives either the v1 descriptor (double SHA-512) or the v2 identifier (HKDF-SHA512 with the kernel's info string) from the unwrapped volume key and compares against the policy. - Install the master key the right way per version: `add_key("logon", ...)` to thread+user keyrings for v1, `FS_IOC_ADD_ENCRYPTION_KEY` for v2. - `home_flush_keyring_fscrypt` opens the image directory, reads the policy version, and either calls `FS_IOC_REMOVE_ENCRYPTION_KEY` (v2) or walks the user keyring (v1). - New homes default to v2; fall back to v1 only when the kernel rejects v2. The on-disk slot xattr format is unchanged - the volume key is the same, only how it binds to the directory changes. There is no v1 -> v2 migration; existing v1 homes continue to unlock, rekey, and deactivate as before.
This commit is contained in:
@@ -46,7 +46,7 @@ REQUIREMENTS:
|
||||
≥ 5.2 for cgroup freezer and new mount API
|
||||
≥ 5.3 for bounded loops in BPF program, keyring namespacing,
|
||||
and nexthop support
|
||||
≥ 5.4 for pidfd and signed Verity images
|
||||
≥ 5.4 for pidfd, signed Verity images, and fscrypt v2 policies
|
||||
≥ 5.6 for getrandom() GRND_INSECURE
|
||||
≥ 5.7 for CLONE_INTO_CGROUP, cgroup2fs memory_recursiveprot option,
|
||||
BPF links and the BPF LSM hook
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "fd-util.h"
|
||||
#include "homework-blob.h"
|
||||
#include "homework-directory.h"
|
||||
#include "homework-fscrypt.h"
|
||||
#include "homework-mount.h"
|
||||
#include "homework-quota.h"
|
||||
#include "log.h"
|
||||
@@ -103,6 +104,11 @@ int home_activate_directory(
|
||||
|
||||
setup->undo_mount = false;
|
||||
|
||||
/* The home is now activated and staying mounted, so it owns any v2 fscrypt master key
|
||||
* home_setup() installed. Disarm the rollback so home_setup_done() doesn't remove the key out
|
||||
* from under the live mount. (No-op for v1 or non-fscrypt storage.) */
|
||||
fscrypt_v2_key_undo_disarm(&setup->fscrypt_v2_key_undo);
|
||||
|
||||
setup->do_drop_caches = false;
|
||||
|
||||
log_info("Everything completed.");
|
||||
|
||||
@@ -9,3 +9,4 @@ typedef enum HomeSetupFlags HomeSetupFlags;
|
||||
|
||||
typedef struct PasswordCache PasswordCache;
|
||||
typedef struct HomeSetup HomeSetup;
|
||||
typedef struct FscryptV2KeyUndo FscryptV2KeyUndo;
|
||||
|
||||
+636
-168
File diff suppressed because it is too large
Load Diff
@@ -3,10 +3,16 @@
|
||||
|
||||
#include "homework-forward.h"
|
||||
|
||||
int home_setup_fscrypt(UserRecord *h, HomeSetup *setup, const PasswordCache *cache);
|
||||
int home_setup_fscrypt(UserRecord *h, HomeSetupFlags flags, HomeSetup *setup, const PasswordCache *cache);
|
||||
|
||||
int home_create_fscrypt(UserRecord *h, HomeSetup *setup, char **effective_passwords, UserRecord **ret_home);
|
||||
|
||||
int home_passwd_fscrypt(UserRecord *h, HomeSetup *setup, const PasswordCache *cache, char **effective_passwords);
|
||||
|
||||
int home_flush_keyring_fscrypt(UserRecord *h);
|
||||
|
||||
/* Fire the pending v2 master-key rollback (remove the key from the filesystem keyring) if armed; called
|
||||
* from home_setup_done() on every teardown/error path. No-op if unarmed or v1. */
|
||||
void fscrypt_v2_key_undo_done(FscryptV2KeyUndo *u);
|
||||
/* Cancel a pending v2 rollback without removing the key: the home is staying active and now owns it. */
|
||||
void fscrypt_v2_key_undo_disarm(FscryptV2KeyUndo *u);
|
||||
|
||||
+6
-1
@@ -459,6 +459,11 @@ int home_setup_done(HomeSetup *setup) {
|
||||
|
||||
setup->key_serial = keyring_unlink(setup->key_serial);
|
||||
|
||||
/* Roll back a v2 fscrypt master key that home_setup_fscrypt() installed but that no activated
|
||||
* home ended up owning (passwd/update/resize of an inactive home, or any error path). On the
|
||||
* activation path home_activate_directory() disarms this first, so the live home keeps its key. */
|
||||
fscrypt_v2_key_undo_done(&setup->fscrypt_v2_key_undo);
|
||||
|
||||
setup->undo_mount = false;
|
||||
setup->undo_dm = false;
|
||||
setup->do_offline_fitrim = false;
|
||||
@@ -514,7 +519,7 @@ int home_setup(
|
||||
break;
|
||||
|
||||
case USER_FSCRYPT:
|
||||
r = home_setup_fscrypt(h, setup, cache);
|
||||
r = home_setup_fscrypt(h, flags, setup, cache);
|
||||
break;
|
||||
|
||||
case USER_CIFS:
|
||||
|
||||
+18
-1
@@ -9,6 +9,21 @@
|
||||
#include "homework-forward.h"
|
||||
#include "user-record-util.h"
|
||||
|
||||
/* RAII rollback state for a v2 fscrypt master-key installation. v2 keys live in the persistent
|
||||
* filesystem keyring (removed only via FS_IOC_REMOVE_ENCRYPTION_KEY), so any setup that installs one
|
||||
* but then tears the home back down instead of activating it must remove the key again, or it lingers
|
||||
* filesystem-wide until the next deactivation or reboot. Armed by home_setup_fscrypt(), fired by
|
||||
* home_setup_done() on every teardown/error path, and disarmed only once a home stays active (see
|
||||
* home_activate_directory()). 'dir_fd' is a dup taken at arm time so the removal target is pinned to
|
||||
* the superblock and unaffected by later rename()/mount changes. (v1 keys go to the per-thread keyring
|
||||
* and die with the homework process, so they need none of this.) */
|
||||
typedef struct FscryptV2KeyUndo {
|
||||
int dir_fd;
|
||||
uint8_t identifier[FSCRYPT_KEY_IDENTIFIER_SIZE];
|
||||
} FscryptV2KeyUndo;
|
||||
|
||||
#define FSCRYPT_V2_KEY_UNDO_INIT { .dir_fd = -EBADF }
|
||||
|
||||
typedef struct HomeSetup {
|
||||
char *dm_name; /* "home-<username>" */
|
||||
char *dm_node; /* "/dev/mapper/home-<username>" */
|
||||
@@ -21,7 +36,8 @@ typedef struct HomeSetup {
|
||||
sd_id128_t found_luks_uuid;
|
||||
sd_id128_t found_fs_uuid;
|
||||
|
||||
uint8_t fscrypt_key_descriptor[FS_KEY_DESCRIPTOR_SIZE];
|
||||
struct fscrypt_key_specifier fscrypt_key_spec;
|
||||
FscryptV2KeyUndo fscrypt_v2_key_undo;
|
||||
|
||||
void *volume_key;
|
||||
size_t volume_key_size;
|
||||
@@ -47,6 +63,7 @@ typedef struct HomeSetup {
|
||||
{ \
|
||||
.root_fd = -EBADF, \
|
||||
.image_fd = -EBADF, \
|
||||
.fscrypt_v2_key_undo = FSCRYPT_V2_KEY_UNDO_INIT, \
|
||||
.partition_offset = UINT64_MAX, \
|
||||
.partition_size = UINT64_MAX, \
|
||||
.key_serial = -1, \
|
||||
|
||||
+43
-31
@@ -1343,10 +1343,13 @@ int kdf_argon2id_derive(
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Perform HKDF-SHA256 derivation, producing derive_size bytes of output.
|
||||
/* Perform HKDF (RFC 5869). 'key' is the input keying material (required, non-empty). 'salt' and 'info'
|
||||
* are optional: pass NULL or an empty iovec to omit. The output is written to 'ret' as a fresh allocation
|
||||
* of 'derive_size' bytes (must be ≤ 255 × HashLen).
|
||||
*
|
||||
* For more details see: https://docs.openssl.org/master/man7/EVP_KDF-HKDF.html */
|
||||
int kdf_hkdf_sha256(
|
||||
* For more details see: https://www.openssl.org/docs/manmaster/man7/EVP_KDF-HKDF.html */
|
||||
int kdf_hkdf_derive(
|
||||
const char *digest,
|
||||
const struct iovec *key,
|
||||
const struct iovec *salt,
|
||||
const struct iovec *info,
|
||||
@@ -1355,9 +1358,10 @@ int kdf_hkdf_sha256(
|
||||
|
||||
int r;
|
||||
|
||||
assert(!key || key->iov_len > 0);
|
||||
assert(!salt || salt->iov_len > 0);
|
||||
assert(!info || info->iov_len > 0);
|
||||
assert(digest);
|
||||
assert(iovec_is_set(key));
|
||||
assert(iovec_is_valid(salt));
|
||||
assert(iovec_is_valid(info));
|
||||
assert(derive_size > 0);
|
||||
assert(ret);
|
||||
|
||||
@@ -1365,9 +1369,9 @@ int kdf_hkdf_sha256(
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
_cleanup_(EVP_KDF_freep) EVP_KDF *kdf = sym_EVP_KDF_fetch(/* propq= */ NULL, "HKDF", /* propq= */ NULL);
|
||||
_cleanup_(EVP_KDF_freep) EVP_KDF *kdf = sym_EVP_KDF_fetch(/* libctx= */ NULL, "HKDF", /* properties= */ NULL);
|
||||
if (!kdf)
|
||||
return log_openssl_errors(LOG_DEBUG, "Failed to create new EVP_KDF for HKDF");
|
||||
return log_openssl_errors(LOG_DEBUG, "Failed to create new EVP_KDF");
|
||||
|
||||
_cleanup_(EVP_KDF_CTX_freep) EVP_KDF_CTX *ctx = sym_EVP_KDF_CTX_new(kdf);
|
||||
if (!ctx)
|
||||
@@ -1377,38 +1381,46 @@ int kdf_hkdf_sha256(
|
||||
if (!bld)
|
||||
return log_openssl_errors(LOG_DEBUG, "Failed to create new OSSL_PARAM_BLD");
|
||||
|
||||
if (!sym_OSSL_PARAM_BLD_push_utf8_string(bld, OSSL_KDF_PARAM_DIGEST, (char*) digest, /* bsize= */ 0))
|
||||
return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF OSSL_KDF_PARAM_DIGEST");
|
||||
|
||||
if (!sym_OSSL_PARAM_BLD_push_octet_string(bld, OSSL_KDF_PARAM_KEY, key->iov_base, key->iov_len))
|
||||
return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF OSSL_KDF_PARAM_KEY");
|
||||
|
||||
if (iovec_is_set(salt))
|
||||
if (!sym_OSSL_PARAM_BLD_push_octet_string(bld, OSSL_KDF_PARAM_SALT, salt->iov_base, salt->iov_len))
|
||||
return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF OSSL_KDF_PARAM_SALT");
|
||||
|
||||
if (iovec_is_set(info))
|
||||
if (!sym_OSSL_PARAM_BLD_push_octet_string(bld, OSSL_KDF_PARAM_INFO, info->iov_base, info->iov_len))
|
||||
return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF OSSL_KDF_PARAM_INFO");
|
||||
|
||||
_cleanup_(OSSL_PARAM_freep) OSSL_PARAM *params = sym_OSSL_PARAM_BLD_to_param(bld);
|
||||
if (!params)
|
||||
return log_openssl_errors(LOG_DEBUG, "Failed to build HKDF OSSL_PARAM");
|
||||
|
||||
_cleanup_(erase_and_freep) void *buf = malloc(derive_size);
|
||||
if (!buf)
|
||||
return log_oom_debug();
|
||||
|
||||
if (!sym_OSSL_PARAM_BLD_push_utf8_string(bld, "digest", "SHA256", 0))
|
||||
return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF digest");
|
||||
|
||||
if (key)
|
||||
if (!sym_OSSL_PARAM_BLD_push_octet_string(bld, "key", key->iov_base, key->iov_len))
|
||||
return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF key");
|
||||
|
||||
if (salt)
|
||||
if (!sym_OSSL_PARAM_BLD_push_octet_string(bld, "salt", salt->iov_base, salt->iov_len))
|
||||
return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF salt");
|
||||
|
||||
if (info)
|
||||
if (!sym_OSSL_PARAM_BLD_push_octet_string(bld, "info", info->iov_base, info->iov_len))
|
||||
return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF info");
|
||||
|
||||
_cleanup_(OSSL_PARAM_freep) OSSL_PARAM *openssl_params = sym_OSSL_PARAM_BLD_to_param(bld);
|
||||
if (!openssl_params)
|
||||
return log_openssl_errors(LOG_DEBUG, "Failed to build HKDF OSSL_PARAM");
|
||||
|
||||
if (sym_EVP_KDF_derive(ctx, buf, derive_size, openssl_params) <= 0)
|
||||
if (sym_EVP_KDF_derive(ctx, buf, derive_size, params) <= 0)
|
||||
return log_openssl_errors(LOG_DEBUG, "OpenSSL HKDF derive failed");
|
||||
|
||||
ret->iov_base = TAKE_PTR(buf);
|
||||
ret->iov_len = derive_size;
|
||||
|
||||
*ret = IOVEC_MAKE(TAKE_PTR(buf), derive_size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Perform HKDF-SHA256 derivation, producing derive_size bytes of output. */
|
||||
int kdf_hkdf_sha256(
|
||||
const struct iovec *key,
|
||||
const struct iovec *salt,
|
||||
const struct iovec *info,
|
||||
size_t derive_size,
|
||||
struct iovec *ret) {
|
||||
|
||||
return kdf_hkdf_derive("SHA256", key, salt, info, derive_size, ret);
|
||||
}
|
||||
|
||||
/* Encrypt the key data using RSA-OAEP with the provided label and specified digest algorithm. Returns 0 on
|
||||
* success, -EOPNOTSUPP if the digest algorithm is not supported, or < 0 for any other error. */
|
||||
int rsa_oaep_encrypt_bytes(
|
||||
|
||||
@@ -392,6 +392,8 @@ int kdf_ss_derive(const char *digest, const void *key, size_t key_size, const vo
|
||||
|
||||
int kdf_kb_hmac_derive(const char *mode, const char *digest, const void *key, size_t key_size, const void *salt, size_t salt_size, const void *info, size_t info_size, const void *seed, size_t seed_size, size_t derive_size, void **ret);
|
||||
|
||||
int kdf_hkdf_derive(const char *digest, const struct iovec *key, const struct iovec *salt, const struct iovec *info, size_t derive_size, struct iovec *ret);
|
||||
|
||||
int rsa_oaep_encrypt_bytes(const EVP_PKEY *pkey, const char *digest_alg, const char *label, const void *decrypted_key, size_t decrypted_key_size, void **ret_encrypt_key, size_t *ret_encrypt_key_size);
|
||||
|
||||
int rsa_pkey_to_suitable_key_size(EVP_PKEY *pkey, size_t *ret_suitable_key_size);
|
||||
|
||||
@@ -342,6 +342,106 @@ TEST(kdf_ss_derive) {
|
||||
"30EB1A1E9DEA7DE4DDB8F3FDF50A01E30581D606C1228D98AFF691DF743AC2EE9D99EFD2AE1946C079AA18C9524877FA65D5065F0DAED058AB3416AF80EB2B73");
|
||||
}
|
||||
|
||||
static void check_hkdf_derive(
|
||||
const char *digest,
|
||||
const char *hex_key,
|
||||
const char *hex_salt,
|
||||
const char *hex_info,
|
||||
const char *hex_expected) {
|
||||
|
||||
DEFINE_HEX_PTR(key, hex_key);
|
||||
DEFINE_HEX_PTR(salt, hex_salt);
|
||||
DEFINE_HEX_PTR(info, hex_info);
|
||||
DEFINE_HEX_PTR(expected, hex_expected);
|
||||
|
||||
_cleanup_(iovec_done) struct iovec derived = {};
|
||||
assert_se(kdf_hkdf_derive(
|
||||
digest,
|
||||
&IOVEC_MAKE(key, key_len),
|
||||
&IOVEC_MAKE(salt, salt_len),
|
||||
&IOVEC_MAKE(info, info_len),
|
||||
expected_len,
|
||||
&derived) >= 0);
|
||||
assert_se(memcmp_nn(derived.iov_base, derived.iov_len, expected, expected_len) == 0);
|
||||
}
|
||||
|
||||
TEST(kdf_hkdf_derive) {
|
||||
/* RFC 5869, Appendix A.1: basic HKDF-SHA256 test vector. */
|
||||
check_hkdf_derive(
|
||||
"SHA256",
|
||||
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
|
||||
"000102030405060708090a0b0c",
|
||||
"f0f1f2f3f4f5f6f7f8f9",
|
||||
"3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865");
|
||||
|
||||
/* fscrypt v2 master-key identifier derivation. The Linux kernel uses standard HKDF-SHA512
|
||||
* (RFC 5869) with empty salt (i.e. HashLen=64 zero bytes per RFC 5869 §2.2), but splits the
|
||||
* info string for code organisation: fs/crypto/hkdf.c's fscrypt_hkdf_expand() hard-codes an
|
||||
* 8-byte "fscrypt\0" prefix, then appends a context byte and the per-caller info argument
|
||||
* before HKDF-Expand. For the key identifier the caller passes context=0x01
|
||||
* (HKDF_CONTEXT_KEY_IDENTIFIER) with empty info, so the effective standard-HKDF info is the
|
||||
* 9-byte string "fscrypt\0\x01". The vectors below assert that against representative IKM
|
||||
* sizes (all-zeros, full 64-byte master key, AES-256 sized, and an arbitrary 64-byte
|
||||
* payload). */
|
||||
const char *fscrypt_info_hex = "667363727970740001";
|
||||
|
||||
check_hkdf_derive(
|
||||
"SHA512",
|
||||
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
/* hex_salt= */ NULL,
|
||||
fscrypt_info_hex,
|
||||
"69d7f347a3ca7bfa3e0c1d84e476d050");
|
||||
|
||||
check_hkdf_derive(
|
||||
"SHA512",
|
||||
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
|
||||
/* hex_salt= */ NULL,
|
||||
fscrypt_info_hex,
|
||||
"8699c2c53707405da5aba5ae4d8583c0");
|
||||
|
||||
check_hkdf_derive(
|
||||
"SHA512",
|
||||
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
|
||||
/* hex_salt= */ NULL,
|
||||
fscrypt_info_hex,
|
||||
"37d7d76a59400083289c185526730d34");
|
||||
|
||||
check_hkdf_derive(
|
||||
"SHA512",
|
||||
"73797374656d642d686f6d6564207632207465737420766563746f72206b65790000000000000000000000000000000000000000000000000000000000000000",
|
||||
/* hex_salt= */ NULL,
|
||||
fscrypt_info_hex,
|
||||
"ab8550968fca25b08222de0ffb7b2986");
|
||||
|
||||
/* Exercise the salt=NULL path in kdf_hkdf_derive directly. DEFINE_HEX_PTR(NULL) allocates a
|
||||
* 1-byte buffer of size 0, so check_hkdf_derive() above feeds the function an
|
||||
* iovec_is_valid()-but-not-iovec_is_set() iovec. Passing salt=NULL outright takes the other
|
||||
* branch. Per RFC 5869 §2.2 an absent salt is identical to HashLen zero bytes, which is also
|
||||
* what OpenSSL substitutes for an empty-octet-string salt, so the derived output must match
|
||||
* byte-for-byte against the third vector above. */
|
||||
{
|
||||
static const uint8_t ikm[] = {
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
|
||||
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
|
||||
0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
|
||||
};
|
||||
static const uint8_t info[] = {
|
||||
'f', 's', 'c', 'r', 'y', 'p', 't', 0x00, 0x01,
|
||||
};
|
||||
DEFINE_HEX_PTR(expected, "37d7d76a59400083289c185526730d34");
|
||||
_cleanup_(iovec_done) struct iovec derived = {};
|
||||
|
||||
assert_se(kdf_hkdf_derive(
|
||||
"SHA512",
|
||||
&IOVEC_MAKE((void*) ikm, sizeof(ikm)),
|
||||
/* salt= */ NULL,
|
||||
&IOVEC_MAKE((void*) info, sizeof(info)),
|
||||
expected_len, &derived) >= 0);
|
||||
assert_se(memcmp_nn(derived.iov_base, derived.iov_len, expected, expected_len) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void check_cipher(
|
||||
const char *alg,
|
||||
size_t bits,
|
||||
|
||||
Reference in New Issue
Block a user