creds: decode Base64 when reading encrypted credentials
Encrypted credentials produced by systemd-creds are stored as Base64. Decode them before decrypting in read_credential_with_decryption(), while leaving plaintext credentials unchanged and retaining their precedence. Bound the decoded encrypted input and report oversized credentials explicitly. Match the plaintext reader's string semantics by rejecting embedded NUL bytes when no size output is requested, while preserving binary payloads for callers that request their size. Add regression coverage for missing and malformed credentials, plaintext precedence, decoded size boundaries, single-line and wrapped Base64, empty strings, and binary data. Keep test outputs and host secrets scoped to private fixtures. Fixes #43621
This commit is contained in:
+18
-8
@@ -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 */
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user