diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index d99369c809..7fbddf7a27 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -180,7 +180,7 @@ int read_credential(const char *name, void **ret, size_t *ret_size) { } int read_credential_with_decryption(const char *name, void **ret, size_t *ret_size) { - _cleanup_(iovec_done_erase) struct iovec ret_iovec = {}; + _cleanup_(iovec_done_erase) struct iovec decrypted = {}; _cleanup_(erase_and_freep) void *data = NULL; _cleanup_free_ char *fn = NULL; size_t sz = 0; @@ -218,16 +218,20 @@ int read_credential_with_decryption(const char *name, void **ret, size_t *ret_si if (!fn) return log_oom(); + /* Encrypted credentials are stored as Base64, unlike the plaintext credentials checked above. */ r = read_full_file_full( AT_FDCWD, fn, - UINT64_MAX, SIZE_MAX, - READ_FULL_FILE_SECURE, + UINT64_MAX, CREDENTIAL_ENCRYPTED_SIZE_MAX, + READ_FULL_FILE_SECURE|READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, (char**) &data, &sz); if (r == -ENOENT) goto not_found; + if (r == -E2BIG) + return log_error_errno(r, "Encrypted credential '%s' exceeds the size limit.", name); if (r < 0) - return log_error_errno(r, "Failed to read encrypted credential data: %m"); + return log_error_errno( + r, "Failed to read or Base64-decode encrypted credential '%s': %m", name); if (geteuid() != 0) r = ipc_decrypt_credential( @@ -236,7 +240,7 @@ int read_credential_with_decryption(const char *name, void **ret, size_t *ret_si getuid(), &IOVEC_MAKE(data, sz), CREDENTIAL_ANY_SCOPE, - &ret_iovec); + &decrypted); else r = decrypt_credential_and_warn( name, @@ -246,14 +250,20 @@ int read_credential_with_decryption(const char *name, void **ret, size_t *ret_si getuid(), &IOVEC_MAKE(data, sz), CREDENTIAL_ANY_SCOPE, - &ret_iovec); + &decrypted); if (r < 0) return r; + /* Match read_credential(): without a size output, the caller expects a string. */ + if (!ret_size && decrypted.iov_len > 0 && memchr(decrypted.iov_base, 0, decrypted.iov_len)) + return log_error_errno( + SYNTHETIC_ERRNO(EBADMSG), + "Decrypted credential '%s' contains embedded NUL bytes, refusing.", name); + if (ret) - *ret = TAKE_PTR(ret_iovec.iov_base); + *ret = TAKE_PTR(decrypted.iov_base); if (ret_size) - *ret_size = ret_iovec.iov_len; + *ret_size = decrypted.iov_len; return 1; /* found */ diff --git a/src/test/test-creds.c b/src/test/test-creds.c index bd18e1c2c2..661a766502 100644 --- a/src/test/test-creds.c +++ b/src/test/test-creds.c @@ -1,13 +1,17 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "alloc-util.h" #include "creds-util.h" +#include "env-util.h" #include "fileio.h" #include "format-util.h" #include "hexdecoct.h" #include "id128-util.h" #include "iovec-util.h" +#include "memory-util.h" #include "path-util.h" #include "rm-rf.h" +#include "string-util.h" #include "tests.h" #include "tmpfile-util.h" #include "tpm2-util.h" @@ -73,6 +77,209 @@ TEST(read_credential_strings) { ASSERT_OK_ERRNO(unsetenv("CREDENTIALS_DIRECTORY")); } +TEST(read_credential_with_decryption) { + static const struct iovec binary = CONST_IOVEC_MAKE_STRING("foo\0bar\n"); + _cleanup_(rm_rf_physical_and_freep) char *plain_dir = NULL, *encrypted_dir = NULL; + _cleanup_(rm_rf_physical_and_freep) char *secret_dir = NULL; + _cleanup_free_ char *saved_plain = NULL, *saved_encrypted = NULL, *saved_secret = NULL; + _cleanup_free_ char *plain_path = NULL, *encrypted_path = NULL, *secret_path = NULL; + void *missing = POINTER_MAX; + size_t missing_size = SIZE_MAX; + + ASSERT_OK(free_and_strdup(&saved_plain, getenv("CREDENTIALS_DIRECTORY"))); + ASSERT_OK(free_and_strdup(&saved_encrypted, getenv("ENCRYPTED_CREDENTIALS_DIRECTORY"))); + ASSERT_OK(free_and_strdup(&saved_secret, getenv("SYSTEMD_CREDENTIAL_SECRET"))); + ASSERT_OK(mkdtemp_malloc(NULL, &plain_dir)); + ASSERT_OK(mkdtemp_malloc(NULL, &encrypted_dir)); + ASSERT_OK(mkdtemp_malloc(NULL, &secret_dir)); + ASSERT_NOT_NULL(secret_path = path_join(secret_dir, "secret")); + ASSERT_OK_ERRNO(setenv("CREDENTIALS_DIRECTORY", plain_dir, /* overwrite= */ true)); + ASSERT_OK_ERRNO(setenv("ENCRYPTED_CREDENTIALS_DIRECTORY", encrypted_dir, /* overwrite= */ true)); + ASSERT_OK_ERRNO(setenv("SYSTEMD_CREDENTIAL_SECRET", secret_path, /* overwrite= */ true)); + + ASSERT_OK_ZERO(read_credential_with_decryption("missing", &missing, &missing_size)); + ASSERT_NULL(missing); + ASSERT_EQ(missing_size, 0U); + + ASSERT_NOT_NULL(encrypted_path = path_join(encrypted_dir, "foo")); + ASSERT_OK(write_string_file(encrypted_path, "!", WRITE_STRING_FILE_CREATE)); + { + _cleanup_(iovec_done_erase) struct iovec result = {}; + + ASSERT_ERROR(read_credential_with_decryption("foo", &result.iov_base, &result.iov_len), + EINVAL); + } + + /* Reject oversized decoded data even when the Base64 encoding is well-formed. */ + { + _cleanup_free_ void *oversized = NULL; + + ASSERT_NOT_NULL(oversized = malloc0(CREDENTIAL_ENCRYPTED_SIZE_MAX + 1)); + FOREACH_ELEMENT(line_break, ((const size_t[]) { SIZE_MAX, 79 })) { + _cleanup_free_ char *encoded = NULL; + _cleanup_(iovec_done_erase) struct iovec result = {}; + + ASSERT_OK(base64mem_full(oversized, CREDENTIAL_ENCRYPTED_SIZE_MAX + 1, + *line_break, &encoded)); + ASSERT_OK(write_string_file(encrypted_path, encoded, WRITE_STRING_FILE_TRUNCATE)); + ASSERT_ERROR(read_credential_with_decryption("foo", &result.iov_base, + &result.iov_len), E2BIG); + } + } + + /* Plaintext takes precedence over the invalid encrypted credential and is not Base64-decoded. */ + ASSERT_NOT_NULL(plain_path = path_join(plain_dir, "foo")); + ASSERT_OK(write_string_file(plain_path, "Zm9v", + WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_AVOID_NEWLINE)); + { + _cleanup_(iovec_done_erase) struct iovec result = {}; + + ASSERT_OK_EQ(read_credential_with_decryption("foo", &result.iov_base, &result.iov_len), 1); + ASSERT_EQ(result.iov_len, STRLEN("Zm9v")); + ASSERT_STREQ(result.iov_base, "Zm9v"); + } + { + _cleanup_(erase_and_freep) char *text = NULL; + + ASSERT_OK_EQ(read_credential_with_decryption("foo", (void**) &text, + /* ret_size= */ NULL), 1); + ASSERT_STREQ(text, "Zm9v"); + } + + /* Asking for a string must reject embedded NUL bytes, without falling back to the encrypted file. */ + ASSERT_OK(write_data_file_atomic_at(AT_FDCWD, plain_path, &binary, /* flags= */ 0)); + { + _cleanup_(iovec_done_erase) struct iovec result = {}; + + ASSERT_OK_EQ(read_credential_with_decryption("foo", &result.iov_base, &result.iov_len), 1); + ASSERT_TRUE(iovec_equal(&binary, &result)); + } + { + _cleanup_(erase_and_freep) char *text = NULL; + + ASSERT_ERROR(read_credential_with_decryption("foo", (void**) &text, /* ret_size= */ NULL), + EBADMSG); + ASSERT_NULL(text); + } + + ASSERT_OK(set_unset_env("CREDENTIALS_DIRECTORY", saved_plain, /* overwrite= */ true)); + ASSERT_OK(set_unset_env("ENCRYPTED_CREDENTIALS_DIRECTORY", saved_encrypted, /* overwrite= */ true)); + ASSERT_OK(set_unset_env("SYSTEMD_CREDENTIAL_SECRET", saved_secret, /* overwrite= */ true)); +} + +TEST(read_credential_with_decryption_encrypted) { + static const struct iovec plaintext = CONST_IOVEC_MAKE_STRING("foo\0bar\n"); + _cleanup_(rm_rf_physical_and_freep) char *plain_dir = NULL, *encrypted_dir = NULL; + _cleanup_(rm_rf_physical_and_freep) char *secret_dir = NULL; + _cleanup_free_ char *saved_plain = NULL, *saved_encrypted = NULL, *saved_secret = NULL; + _cleanup_free_ char *secret_path = NULL, *credential_path = NULL; + _cleanup_(iovec_done_erase) struct iovec large_plaintext = {}; + int r; + + if (geteuid() != 0) + return (void) log_tests_skipped("reading encrypted credentials directly requires root"); + + ASSERT_OK(free_and_strdup(&saved_plain, getenv("CREDENTIALS_DIRECTORY"))); + ASSERT_OK(free_and_strdup(&saved_encrypted, getenv("ENCRYPTED_CREDENTIALS_DIRECTORY"))); + ASSERT_OK(free_and_strdup(&saved_secret, getenv("SYSTEMD_CREDENTIAL_SECRET"))); + ASSERT_OK(mkdtemp_malloc(NULL, &plain_dir)); + ASSERT_OK(mkdtemp_malloc(NULL, &encrypted_dir)); + ASSERT_OK(mkdtemp_malloc(NULL, &secret_dir)); + ASSERT_NOT_NULL(secret_path = path_join(secret_dir, "secret")); + ASSERT_NOT_NULL(credential_path = path_join(encrypted_dir, "foo")); + ASSERT_OK_ERRNO(setenv("CREDENTIALS_DIRECTORY", plain_dir, /* overwrite= */ true)); + ASSERT_OK_ERRNO(setenv("ENCRYPTED_CREDENTIALS_DIRECTORY", encrypted_dir, /* overwrite= */ true)); + ASSERT_OK_ERRNO(setenv("SYSTEMD_CREDENTIAL_SECRET", secret_path, /* overwrite= */ true)); + + ASSERT_NOT_NULL(large_plaintext.iov_base = malloc0(CREDENTIAL_SIZE_MAX)); + large_plaintext.iov_len = CREDENTIAL_SIZE_MAX; + + const struct { + struct iovec data; + bool contains_nul; + } inputs[] = { + { .data = CONST_IOVEC_MAKE_STRING("Zm9v"), .contains_nul = false }, + { .data = CONST_IOVEC_MAKE_STRING(""), .contains_nul = false }, + { .data = plaintext, .contains_nul = true }, + { .data = large_plaintext, .contains_nul = true }, + }; + + FOREACH_ELEMENT(input, inputs) { + _cleanup_(iovec_done_erase) struct iovec encrypted = {}; + + r = encrypt_credential_and_warn( + CRED_AES256_GCM_BY_HOST, + "foo", + /* timestamp= */ USEC_INFINITY, + /* not_after= */ USEC_INFINITY, + /* tpm2_device= */ NULL, + /* tpm2_hash_pcr_mask= */ 0, + /* tpm2_pubkey_path= */ NULL, + /* tpm2_pubkey_pcr_mask= */ 0, + /* uid= */ UID_INVALID, + &input->data, + /* flags= */ 0, + &encrypted); + if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) { + (void) log_tests_skipped("encrypted credentials are not supported"); + goto finish; + } + if (ERRNO_IS_NEG_MACHINE_ID_UNSET(r)) { + (void) log_tests_skipped("machine ID is not initialized"); + goto finish; + } + ASSERT_OK(r); + + /* Exercise both single-line Base64 and the line wrapping used by systemd-creds. */ + FOREACH_ELEMENT(line_break, ((const size_t[]) { SIZE_MAX, 79 })) { + _cleanup_(erase_and_freep) char *encoded = NULL; + _cleanup_(iovec_done_erase) struct iovec result = {}; + _cleanup_(erase_and_freep) char *text = NULL; + + ASSERT_OK(base64mem_full(encrypted.iov_base, encrypted.iov_len, + *line_break, &encoded)); + if (input->data.iov_len == CREDENTIAL_SIZE_MAX) + ASSERT_GT(strlen(encoded), (size_t) CREDENTIAL_ENCRYPTED_SIZE_MAX); + ASSERT_OK(write_string_file(credential_path, encoded, + WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_TRUNCATE)); + ASSERT_OK_EQ(read_credential_with_decryption("foo", &result.iov_base, + &result.iov_len), 1); + ASSERT_TRUE(iovec_equal(&input->data, &result)); + + r = read_credential_with_decryption("foo", (void**) &text, /* ret_size= */ NULL); + if (input->contains_nul) { + ASSERT_ERROR(r, EBADMSG); + ASSERT_NULL(text); + } else { + ASSERT_OK_EQ(r, 1); + ASSERT_STREQ(text, input->data.iov_base); + } + } + } + + /* At and just below the size limit, valid Base64 still reaches the credential-format parser. */ + { + _cleanup_free_ void *invalid = NULL; + + ASSERT_NOT_NULL(invalid = malloc0(CREDENTIAL_ENCRYPTED_SIZE_MAX)); + FOREACH_ELEMENT(delta, ((const size_t[]) { 1, 0 })) { + _cleanup_free_ char *encoded = NULL; + _cleanup_(iovec_done_erase) struct iovec result = {}; + + ASSERT_OK(base64mem_full(invalid, CREDENTIAL_ENCRYPTED_SIZE_MAX - *delta, + 79, &encoded)); + ASSERT_OK(write_string_file(credential_path, encoded, WRITE_STRING_FILE_TRUNCATE)); + ASSERT_ERROR(read_credential_with_decryption("foo", &result.iov_base, + &result.iov_len), EOPNOTSUPP); + } + } + +finish: + ASSERT_OK(set_unset_env("CREDENTIALS_DIRECTORY", saved_plain, /* overwrite= */ true)); + ASSERT_OK(set_unset_env("ENCRYPTED_CREDENTIALS_DIRECTORY", saved_encrypted, /* overwrite= */ true)); + ASSERT_OK(set_unset_env("SYSTEMD_CREDENTIAL_SECRET", saved_secret, /* overwrite= */ true)); +} + TEST(credential_name_valid) { char buf[NAME_MAX+2];