diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 03:50:40 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 03:50:40 +0000 |
commit | fc53809803cd2bc2434e312b19a18fa36776da12 (patch) | |
tree | b4b43bd6538f51965ce32856e9c053d0f90919c8 /src/creds | |
parent | Adding upstream version 255.5. (diff) | |
download | systemd-fc53809803cd2bc2434e312b19a18fa36776da12.tar.xz systemd-fc53809803cd2bc2434e312b19a18fa36776da12.zip |
Adding upstream version 256.upstream/256
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/creds')
-rw-r--r-- | src/creds/creds.c | 558 | ||||
-rw-r--r-- | src/creds/io.systemd.credentials.policy | 40 | ||||
-rw-r--r-- | src/creds/meson.build | 3 |
3 files changed, 535 insertions, 66 deletions
diff --git a/src/creds/creds.c b/src/creds/creds.c index 10d1171..1c8d957 100644 --- a/src/creds/creds.c +++ b/src/creds/creds.c @@ -4,6 +4,7 @@ #include <unistd.h> #include "build.h" +#include "bus-polkit.h" #include "creds-util.h" #include "dirent-util.h" #include "escape.h" @@ -24,6 +25,9 @@ #include "terminal-util.h" #include "tpm2-pcr.h" #include "tpm2-util.h" +#include "user-util.h" +#include "varlink.h" +#include "varlink-io.systemd.Credentials.h" #include "verbs.h" typedef enum TranscodeMode { @@ -54,6 +58,9 @@ static usec_t arg_timestamp = USEC_INFINITY; static usec_t arg_not_after = USEC_INFINITY; static bool arg_pretty = false; static bool arg_quiet = false; +static bool arg_varlink = false; +static uid_t arg_uid = UID_INVALID; +static bool arg_allow_null = false; STATIC_DESTRUCTOR_REGISTER(arg_tpm2_public_key, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_signature, freep); @@ -228,7 +235,7 @@ static int verb_list(int argc, char **argv, void *userdata) { return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "No credentials passed. (i.e. $CREDENTIALS_DIRECTORY not set.)"); } - if ((arg_json_format_flags & JSON_FORMAT_OFF) && table_get_rows(t) <= 1) { + if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF) && table_isempty(t)) { log_info("No credentials"); return 0; } @@ -311,7 +318,7 @@ static int print_newline(FILE *f, const char *data, size_t l) { /* Don't bother unless this is a tty */ fd = fileno(f); - if (fd >= 0 && isatty(fd) <= 0) + if (fd >= 0 && !isatty_safe(fd)) return 0; if (fputc('\n', f) != '\n') @@ -375,9 +382,7 @@ static int verb_cat(int argc, char **argv, void *userdata) { int encrypted; if (!credential_name_valid(*cn)) { - log_error("Credential name '%s' is not valid.", *cn); - if (ret >= 0) - ret = -EINVAL; + RET_GATHER(ret, log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential name '%s' is not valid.", *cn)); continue; } @@ -402,36 +407,41 @@ static int verb_cat(int argc, char **argv, void *userdata) { if (r >= 0) /* Found */ break; - log_error_errno(r, "Failed to read credential '%s': %m", *cn); - if (ret >= 0) - ret = r; + RET_GATHER(ret, log_error_errno(r, "Failed to read credential '%s': %m", *cn)); } if (encrypted >= 2) { /* Found nowhere */ - log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Credential '%s' not set.", *cn); - if (ret >= 0) - ret = -ENOENT; - + RET_GATHER(ret, log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Credential '%s' not set.", *cn)); continue; } if (encrypted) { - _cleanup_(erase_and_freep) void *plaintext = NULL; - size_t plaintext_size; - - r = decrypt_credential_and_warn( - *cn, - timestamp, - arg_tpm2_device, - arg_tpm2_signature, - data, size, - &plaintext, &plaintext_size); + _cleanup_(iovec_done_erase) struct iovec plaintext = {}; + + if (geteuid() != 0) + r = ipc_decrypt_credential( + *cn, + timestamp, + uid_is_valid(arg_uid) ? arg_uid : getuid(), + &IOVEC_MAKE(data, size), + CREDENTIAL_ANY_SCOPE, + &plaintext); + else + r = decrypt_credential_and_warn( + *cn, + timestamp, + arg_tpm2_device, + arg_tpm2_signature, + uid_is_valid(arg_uid) ? arg_uid : getuid(), + &IOVEC_MAKE(data, size), + CREDENTIAL_ANY_SCOPE, + &plaintext); if (r < 0) return r; erase_and_free(data); - data = TAKE_PTR(plaintext); - size = plaintext_size; + data = TAKE_PTR(plaintext.iov_base); + size = plaintext.iov_len; } r = write_blob(stdout, data, size); @@ -443,11 +453,9 @@ static int verb_cat(int argc, char **argv, void *userdata) { } static int verb_encrypt(int argc, char **argv, void *userdata) { + _cleanup_(iovec_done_erase) struct iovec plaintext = {}, output = {}; _cleanup_free_ char *base64_buf = NULL, *fname = NULL; - _cleanup_(erase_and_freep) char *plaintext = NULL; const char *input_path, *output_path, *name; - _cleanup_free_ void *output = NULL; - size_t plaintext_size, output_size; ssize_t base64_size; usec_t timestamp; int r; @@ -457,9 +465,9 @@ static int verb_encrypt(int argc, char **argv, void *userdata) { input_path = empty_or_dash(argv[1]) ? NULL : argv[1]; if (input_path) - r = read_full_file_full(AT_FDCWD, input_path, UINT64_MAX, CREDENTIAL_SIZE_MAX, READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &plaintext, &plaintext_size); + r = read_full_file_full(AT_FDCWD, input_path, UINT64_MAX, CREDENTIAL_SIZE_MAX, READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, (char**) &plaintext.iov_base, &plaintext.iov_len); else - r = read_full_stream_full(stdin, NULL, UINT64_MAX, CREDENTIAL_SIZE_MAX, READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER, &plaintext, &plaintext_size); + r = read_full_stream_full(stdin, NULL, UINT64_MAX, CREDENTIAL_SIZE_MAX, READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER, (char**) &plaintext.iov_base, &plaintext.iov_len); if (r == -E2BIG) return log_error_errno(r, "Plaintext too long for credential (allowed size: %zu).", (size_t) CREDENTIAL_SIZE_MAX); if (r < 0) @@ -489,21 +497,33 @@ static int verb_encrypt(int argc, char **argv, void *userdata) { if (arg_not_after != USEC_INFINITY && arg_not_after < timestamp) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential is invalidated before it is valid."); - r = encrypt_credential_and_warn( - arg_with_key, - name, - timestamp, - arg_not_after, - arg_tpm2_device, - arg_tpm2_pcr_mask, - arg_tpm2_public_key, - arg_tpm2_public_key_pcr_mask, - plaintext, plaintext_size, - &output, &output_size); + if (geteuid() != 0) + r = ipc_encrypt_credential( + name, + timestamp, + arg_not_after, + arg_uid, + &plaintext, + /* flags= */ 0, + &output); + else + r = encrypt_credential_and_warn( + arg_with_key, + name, + timestamp, + arg_not_after, + arg_tpm2_device, + arg_tpm2_pcr_mask, + arg_tpm2_public_key, + arg_tpm2_public_key_pcr_mask, + arg_uid, + &plaintext, + /* flags= */ 0, + &output); if (r < 0) return r; - base64_size = base64mem_full(output, output_size, arg_pretty ? 69 : 79, &base64_buf); + base64_size = base64mem_full(output.iov_base, output.iov_len, arg_pretty ? 69 : 79, &base64_buf); if (base64_size < 0) return base64_size; @@ -539,11 +559,10 @@ static int verb_encrypt(int argc, char **argv, void *userdata) { } static int verb_decrypt(int argc, char **argv, void *userdata) { - _cleanup_(erase_and_freep) void *plaintext = NULL; - _cleanup_free_ char *input = NULL, *fname = NULL; + _cleanup_(iovec_done_erase) struct iovec input = {}, plaintext = {}; + _cleanup_free_ char *fname = NULL; _cleanup_fclose_ FILE *output_file = NULL; const char *input_path, *output_path, *name; - size_t input_size, plaintext_size; usec_t timestamp; FILE *f; int r; @@ -553,9 +572,9 @@ static int verb_decrypt(int argc, char **argv, void *userdata) { input_path = empty_or_dash(argv[1]) ? NULL : argv[1]; if (input_path) - r = read_full_file_full(AT_FDCWD, argv[1], UINT64_MAX, CREDENTIAL_ENCRYPTED_SIZE_MAX, READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &input, &input_size); + r = read_full_file_full(AT_FDCWD, argv[1], UINT64_MAX, CREDENTIAL_ENCRYPTED_SIZE_MAX, READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, (char**) &input, &input.iov_len); else - r = read_full_stream_full(stdin, NULL, UINT64_MAX, CREDENTIAL_ENCRYPTED_SIZE_MAX, READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, &input, &input_size); + r = read_full_stream_full(stdin, NULL, UINT64_MAX, CREDENTIAL_ENCRYPTED_SIZE_MAX, READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, (char**) &input, &input.iov_len); if (r == -E2BIG) return log_error_errno(r, "Data too long for encrypted credential (allowed size: %zu).", (size_t) CREDENTIAL_ENCRYPTED_SIZE_MAX); if (r < 0) @@ -582,13 +601,24 @@ static int verb_decrypt(int argc, char **argv, void *userdata) { timestamp = arg_timestamp != USEC_INFINITY ? arg_timestamp : now(CLOCK_REALTIME); - r = decrypt_credential_and_warn( - name, - timestamp, - arg_tpm2_device, - arg_tpm2_signature, - input, input_size, - &plaintext, &plaintext_size); + if (geteuid() != 0) + r = ipc_decrypt_credential( + name, + timestamp, + arg_uid, + &input, + /* flags= */ 0, + &plaintext); + else + r = decrypt_credential_and_warn( + name, + timestamp, + arg_tpm2_device, + arg_tpm2_signature, + arg_uid, + &input, + arg_allow_null ? CREDENTIAL_ALLOW_NULL : 0, + &plaintext); if (r < 0) return r; @@ -601,7 +631,7 @@ static int verb_decrypt(int argc, char **argv, void *userdata) { } else f = stdout; - r = write_blob(f, plaintext, plaintext_size); + r = write_blob(f, plaintext.iov_base, plaintext.iov_len); if (r < 0) return r; @@ -609,14 +639,14 @@ static int verb_decrypt(int argc, char **argv, void *userdata) { } static int verb_setup(int argc, char **argv, void *userdata) { - size_t size; + _cleanup_(iovec_done_erase) struct iovec host_key = {}; int r; - r = get_credential_host_secret(CREDENTIAL_SECRET_GENERATE|CREDENTIAL_SECRET_WARN_NOT_ENCRYPTED, NULL, &size); + r = get_credential_host_secret(CREDENTIAL_SECRET_GENERATE|CREDENTIAL_SECRET_WARN_NOT_ENCRYPTED, &host_key); if (r < 0) return log_error_errno(r, "Failed to setup credentials host key: %m"); - log_info("%zu byte credentials host key set up.", size); + log_info("%zu byte credentials host key set up.", host_key.iov_len); return EXIT_SUCCESS; } @@ -689,7 +719,7 @@ static int verb_help(int argc, char **argv, void *userdata) { " --timestamp=TIME Include specified timestamp in encrypted credential\n" " --not-after=TIME Include specified invalidation time in encrypted\n" " credential\n" - " --with-key=host|tpm2|host+tpm2|tpm2-absent|auto|auto-initrd\n" + " --with-key=host|tpm2|host+tpm2|null|auto|auto-initrd\n" " Which keys to encrypt with\n" " -H Shortcut for --with-key=host\n" " -T Shortcut for --with-key=tpm2\n" @@ -703,13 +733,17 @@ static int verb_help(int argc, char **argv, void *userdata) { " Specify TPM2 PCRs to seal against (public key)\n" " --tpm2-signature=PATH\n" " Specify signature for public key PCR policy\n" + " --user Select user-scoped credential encryption\n" + " --uid=UID Select user for scoped credentials\n" + " --allow-null Allow decrypting credentials with empty key\n" " -q --quiet Suppress output for 'has-tpm2' verb\n" - "\nSee the %2$s for details.\n" - , program_invocation_short_name - , link - , ansi_underline(), ansi_normal() - , ansi_highlight(), ansi_normal() - ); + "\nSee the %2$s for details.\n", + program_invocation_short_name, + link, + ansi_underline(), + ansi_normal(), + ansi_highlight(), + ansi_normal()); return 0; } @@ -733,6 +767,9 @@ static int parse_argv(int argc, char *argv[]) { ARG_NAME, ARG_TIMESTAMP, ARG_NOT_AFTER, + ARG_USER, + ARG_UID, + ARG_ALLOW_NULL, }; static const struct option options[] = { @@ -755,6 +792,9 @@ static int parse_argv(int argc, char *argv[]) { { "timestamp", required_argument, NULL, ARG_TIMESTAMP }, { "not-after", required_argument, NULL, ARG_NOT_AFTER }, { "quiet", no_argument, NULL, 'q' }, + { "user", no_argument, NULL, ARG_USER }, + { "uid", required_argument, NULL, ARG_UID }, + { "allow-null", no_argument, NULL, ARG_ALLOW_NULL }, {} }; @@ -838,8 +878,8 @@ static int parse_argv(int argc, char *argv[]) { arg_with_key = CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC; else if (STR_IN_SET(optarg, "host+tpm2-with-public-key", "tpm2-with-public-key+host")) arg_with_key = CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK; - else if (streq(optarg, "tpm2-absent")) - arg_with_key = CRED_AES256_GCM_BY_TPM2_ABSENT; + else if (STR_IN_SET(optarg, "null", "tpm2-absent")) + arg_with_key = CRED_AES256_GCM_BY_NULL; else return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown key type: %s", optarg); @@ -916,6 +956,36 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_USER: + if (!uid_is_valid(arg_uid)) + arg_uid = getuid(); + + break; + + case ARG_UID: + if (isempty(optarg)) + arg_uid = UID_INVALID; + else if (streq(optarg, "self")) + arg_uid = getuid(); + else { + const char *name = optarg; + + r = get_user_creds( + &name, + &arg_uid, + /* ret_gid= */ NULL, + /* ret_home= */ NULL, + /* ret_shell= */ NULL, + /* flags= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to resolve user '%s': %m", optarg); + } + break; + + case ARG_ALLOW_NULL: + arg_allow_null = true; + break; + case 'q': arg_quiet = true; break; @@ -928,11 +998,31 @@ static int parse_argv(int argc, char *argv[]) { } } + if (uid_is_valid(arg_uid)) { + /* If a UID is specified, then switch to scoped credentials */ + + if (sd_id128_equal(arg_with_key, _CRED_AUTO)) + arg_with_key = _CRED_AUTO_SCOPED; + else if (sd_id128_in_set(arg_with_key, CRED_AES256_GCM_BY_HOST, CRED_AES256_GCM_BY_HOST_SCOPED)) + arg_with_key = CRED_AES256_GCM_BY_HOST_SCOPED; + else if (sd_id128_in_set(arg_with_key, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED)) + arg_with_key = CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED; + else if (sd_id128_in_set(arg_with_key, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED)) + arg_with_key = CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED; + else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Selected key not available in --uid= scoped mode, refusing."); + } + if (arg_tpm2_pcr_mask == UINT32_MAX) arg_tpm2_pcr_mask = TPM2_PCR_MASK_DEFAULT; if (arg_tpm2_public_key_pcr_mask == UINT32_MAX) arg_tpm2_public_key_pcr_mask = UINT32_C(1) << TPM2_PCR_KERNEL_BOOT; + r = varlink_invocation(VARLINK_ALLOW_ACCEPT); + if (r < 0) + return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m"); + arg_varlink = r; + return 1; } @@ -952,6 +1042,312 @@ static int creds_main(int argc, char *argv[]) { return dispatch_verb(argc, argv, verbs, NULL); } +#define TIMESTAMP_FRESH_MAX (30*USEC_PER_SEC) + +static bool timestamp_is_fresh(usec_t x) { + usec_t n = now(CLOCK_REALTIME); + + /* We'll only allow unprivileged encryption/decryption for somehwhat "fresh" timestamps */ + + if (x > n) + return x - n <= TIMESTAMP_FRESH_MAX; + else + return n - x <= TIMESTAMP_FRESH_MAX; +} + +typedef enum CredentialScope { + CREDENTIAL_SYSTEM, + CREDENTIAL_USER, + /* One day we should add more here, for example, per-app/per-service credentials */ + _CREDENTIAL_SCOPE_MAX, + _CREDENTIAL_SCOPE_INVALID = -EINVAL, +} CredentialScope; + +static const char* credential_scope_table[_CREDENTIAL_SCOPE_MAX] = { + [CREDENTIAL_SYSTEM] = "system", + [CREDENTIAL_USER] = "user", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(credential_scope, CredentialScope); +static JSON_DISPATCH_ENUM_DEFINE(dispatch_credential_scope, CredentialScope, credential_scope_from_string); + +typedef struct MethodEncryptParameters { + const char *name; + const char *text; + struct iovec data; + uint64_t timestamp; + uint64_t not_after; + CredentialScope scope; + uid_t uid; +} MethodEncryptParameters; + +static void method_encrypt_parameters_done(MethodEncryptParameters *p) { + assert(p); + + iovec_done_erase(&p->data); +} + +static int settle_scope( + Varlink *link, + CredentialScope *scope, + uid_t *uid, + CredentialFlags *flags, + bool *any_scope_after_polkit) { + + uid_t peer_uid; + int r; + + assert(link); + assert(scope); + assert(uid); + assert(flags); + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + + if (*scope < 0) { + if (uid_is_valid(*uid)) + *scope = CREDENTIAL_USER; + else { + *scope = CREDENTIAL_SYSTEM; /* When encrypting, we spit out a system credential */ + *uid = peer_uid; /* When decrypting a user credential, use this UID */ + } + + if (peer_uid == 0) + *flags |= CREDENTIAL_ANY_SCOPE; + + if (any_scope_after_polkit) + *any_scope_after_polkit = true; + } else if (*scope == CREDENTIAL_USER) { + if (!uid_is_valid(*uid)) + *uid = peer_uid; + } else { + assert(*scope == CREDENTIAL_SYSTEM); + if (uid_is_valid(*uid)) + return varlink_error_invalid_parameter_name(link, "uid"); + } + + return 0; +} + +static int vl_method_encrypt(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "name", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(MethodEncryptParameters, name), 0 }, + { "text", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(MethodEncryptParameters, text), 0 }, + { "data", JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(MethodEncryptParameters, data), 0 }, + { "timestamp", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(MethodEncryptParameters, timestamp), 0 }, + { "notAfter", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(MethodEncryptParameters, not_after), 0 }, + { "scope", JSON_VARIANT_STRING, dispatch_credential_scope, offsetof(MethodEncryptParameters, scope), 0 }, + { "uid", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uid_gid, offsetof(MethodEncryptParameters, uid), 0 }, + VARLINK_DISPATCH_POLKIT_FIELD, + {} + }; + _cleanup_(method_encrypt_parameters_done) MethodEncryptParameters p = { + .timestamp = UINT64_MAX, + .not_after = UINT64_MAX, + .scope = _CREDENTIAL_SCOPE_INVALID, + .uid = UID_INVALID, + }; + _cleanup_(iovec_done) struct iovec output = {}; + Hashmap **polkit_registry = ASSERT_PTR(userdata); + CredentialFlags cflags = 0; + bool timestamp_fresh; + uid_t peer_uid; + int r; + + assert(link); + + r = varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + if (p.name && !credential_name_valid(p.name)) + return varlink_error_invalid_parameter_name(link, "name"); + /* Specifying both or neither the text string and the binary data is not allowed */ + if (!!p.text == !!p.data.iov_base) + return varlink_error_invalid_parameter_name(link, "data"); + if (p.timestamp == UINT64_MAX) { + p.timestamp = now(CLOCK_REALTIME); + timestamp_fresh = true; + } else + timestamp_fresh = timestamp_is_fresh(p.timestamp); + if (p.not_after != UINT64_MAX && p.not_after < p.timestamp) + return varlink_error_invalid_parameter_name(link, "notAfter"); + + r = settle_scope(link, &p.scope, &p.uid, &cflags, /* any_scope_after_polkit= */ NULL); + if (r < 0) + return r; + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + + /* Relax security requirements if peer wants to encrypt credentials for themselves */ + bool own_scope = p.scope == CREDENTIAL_USER && p.uid == peer_uid; + + if (!own_scope || !timestamp_fresh) { + /* Insist on PK if client wants to encrypt for another user or the system, or if the timestamp was explicitly overridden. */ + r = varlink_verify_polkit_async( + link, + /* bus= */ NULL, + "io.systemd.credentials.encrypt", + /* details= */ NULL, + polkit_registry); + if (r <= 0) + return r; + } + + r = encrypt_credential_and_warn( + p.scope == CREDENTIAL_USER ? _CRED_AUTO_SCOPED : _CRED_AUTO, + p.name, + p.timestamp, + p.not_after, + arg_tpm2_device, + arg_tpm2_pcr_mask, + arg_tpm2_public_key, + arg_tpm2_public_key_pcr_mask, + p.uid, + p.text ? &IOVEC_MAKE_STRING(p.text) : &p.data, + cflags, + &output); + if (r == -ESRCH) + return varlink_error(link, "io.systemd.Credentials.NoSuchUser", NULL); + if (r < 0) + return r; + + _cleanup_(json_variant_unrefp) JsonVariant *reply = NULL; + + r = json_build(&reply, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_IOVEC_BASE64("blob", &output))); + if (r < 0) + return r; + + /* Let's also mark the (theoretically encrypted) reply as sensitive, in case the NULL encryption scheme was used. */ + json_variant_sensitive(reply); + + return varlink_reply(link, reply); +} + +typedef struct MethodDecryptParameters { + const char *name; + struct iovec blob; + uint64_t timestamp; + CredentialScope scope; + uid_t uid; +} MethodDecryptParameters; + +static void method_decrypt_parameters_done(MethodDecryptParameters *p) { + assert(p); + + iovec_done_erase(&p->blob); +} + +static int vl_method_decrypt(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "name", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(MethodDecryptParameters, name), 0 }, + { "blob", JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(MethodDecryptParameters, blob), JSON_MANDATORY }, + { "timestamp", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(MethodDecryptParameters, timestamp), 0 }, + { "scope", JSON_VARIANT_STRING, dispatch_credential_scope, offsetof(MethodDecryptParameters, scope), 0 }, + { "uid", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uid_gid, offsetof(MethodDecryptParameters, uid), 0 }, + VARLINK_DISPATCH_POLKIT_FIELD, + {} + }; + _cleanup_(method_decrypt_parameters_done) MethodDecryptParameters p = { + .timestamp = UINT64_MAX, + .scope = _CREDENTIAL_SCOPE_INVALID, + .uid = UID_INVALID, + }; + bool timestamp_fresh, any_scope_after_polkit = false; + _cleanup_(iovec_done_erase) struct iovec output = {}; + Hashmap **polkit_registry = ASSERT_PTR(userdata); + CredentialFlags cflags = 0; + uid_t peer_uid; + int r; + + assert(link); + + r = varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + if (p.name && !credential_name_valid(p.name)) + return varlink_error_invalid_parameter_name(link, "name"); + if (p.timestamp == UINT64_MAX) { + p.timestamp = now(CLOCK_REALTIME); + timestamp_fresh = true; + } else + timestamp_fresh = timestamp_is_fresh(p.timestamp); + + r = settle_scope(link, &p.scope, &p.uid, &cflags, &any_scope_after_polkit); + if (r < 0) + return r; + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + + /* Relax security requirements if peer wants to encrypt credentials for themselves */ + bool own_scope = p.scope == CREDENTIAL_USER && p.uid == peer_uid; + bool ask_polkit = !own_scope || !timestamp_fresh; + for (;;) { + if (ask_polkit) { + r = varlink_verify_polkit_async( + link, + /* bus= */ NULL, + "io.systemd.credentials.decrypt", + /* details= */ NULL, + polkit_registry); + if (r <= 0) + return r; + + /* Now that we have authenticated, it's fine to allow unpriv clients access to system secrets */ + if (any_scope_after_polkit) + cflags |= CREDENTIAL_ANY_SCOPE; + } + + r = decrypt_credential_and_warn( + p.name, + p.timestamp, + arg_tpm2_device, + arg_tpm2_signature, + p.uid, + &p.blob, + cflags, + &output); + if (r != -EMEDIUMTYPE || ask_polkit || !any_scope_after_polkit) + break; + + /* So the secret was apparently intended for the system. Let's retry decrypting it after + * acquiring polkit's permission. */ + ask_polkit = true; + } + + if (r == -EBADMSG) + return varlink_error(link, "io.systemd.Credentials.BadFormat", NULL); + if (r == -EREMOTE) + return varlink_error(link, "io.systemd.Credentials.NameMismatch", NULL); + if (r == -ESTALE) + return varlink_error(link, "io.systemd.Credentials.TimeMismatch", NULL); + if (r == -ESRCH) + return varlink_error(link, "io.systemd.Credentials.NoSuchUser", NULL); + if (r == -EMEDIUMTYPE) + return varlink_error(link, "io.systemd.Credentials.BadScope", NULL); + if (r < 0) + return r; + + _cleanup_(json_variant_unrefp) JsonVariant *reply = NULL; + + r = json_build(&reply, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_IOVEC_BASE64("data", &output))); + if (r < 0) + return r; + + json_variant_sensitive(reply); + + return varlink_reply(link, reply); +} + static int run(int argc, char *argv[]) { int r; @@ -961,6 +1357,36 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; + if (arg_varlink) { + _cleanup_(varlink_server_unrefp) VarlinkServer *varlink_server = NULL; + _cleanup_(hashmap_freep) Hashmap *polkit_registry = NULL; + + /* Invocation as Varlink service */ + + r = varlink_server_new(&varlink_server, VARLINK_SERVER_ACCOUNT_UID|VARLINK_SERVER_INHERIT_USERDATA|VARLINK_SERVER_INPUT_SENSITIVE); + if (r < 0) + return log_error_errno(r, "Failed to allocate Varlink server: %m"); + + r = varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_Credentials); + if (r < 0) + return log_error_errno(r, "Failed to add Varlink interface: %m"); + + r = varlink_server_bind_method_many( + varlink_server, + "io.systemd.Credentials.Encrypt", vl_method_encrypt, + "io.systemd.Credentials.Decrypt", vl_method_decrypt); + if (r < 0) + return log_error_errno(r, "Failed to bind Varlink methods: %m"); + + varlink_server_set_userdata(varlink_server, &polkit_registry); + + r = varlink_server_loop_auto(varlink_server); + if (r < 0) + return log_error_errno(r, "Failed to run Varlink event loop: %m"); + + return 0; + } + return creds_main(argc, argv); } diff --git a/src/creds/io.systemd.credentials.policy b/src/creds/io.systemd.credentials.policy new file mode 100644 index 0000000..b4e911f --- /dev/null +++ b/src/creds/io.systemd.credentials.policy @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*--> +<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" + "https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd"> + +<!-- + SPDX-License-Identifier: LGPL-2.1-or-later + + This file is part of systemd. + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. +--> + +<policyconfig> + + <vendor>The systemd Project</vendor> + <vendor_url>https://systemd.io</vendor_url> + + <action id="io.systemd.credentials.encrypt"> + <description gettext-domain="systemd">Allow encryption and signing of system credentials.</description> + <message gettext-domain="systemd">Authentication is required for an application to encrypt and sign a system credential.</message> + <defaults> + <allow_any>auth_admin_keep</allow_any> + <allow_inactive>auth_admin_keep</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + </action> + + <action id="io.systemd.credentials.decrypt"> + <description gettext-domain="systemd">Allow decryption of system credentials.</description> + <message gettext-domain="systemd">Authentication is required for an application to decrypt a system credential.</message> + <defaults> + <allow_any>auth_admin_keep</allow_any> + <allow_inactive>auth_admin_keep</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + </action> +</policyconfig> diff --git a/src/creds/meson.build b/src/creds/meson.build index 8557256..2483311 100644 --- a/src/creds/meson.build +++ b/src/creds/meson.build @@ -23,3 +23,6 @@ if install_sysconfdir install_emptydir(sysconfdir / 'credstore.encrypted', install_mode : 'rwx------') endif + +install_data('io.systemd.credentials.policy', + install_dir : polkitpolicydir) |