From: Lennart Poettering Date: Wed, 17 Jan 2024 20:44:01 +0000 (+0100) Subject: creds: allow Varlink clients to encrypt/decrypt their own credentials without polkit... X-Git-Tag: v256-rc1~1009^2~5 X-Git-Url: http://git-history.diyao.me/?a=commitdiff_plain;h=8464f7cbd652af75566017c62bec5308d1c30775;p=systemd%2F.git creds: allow Varlink clients to encrypt/decrypt their own credentials without polkit authentication Now that we have the concept of scoped credentials, we can allow unprivileged clients to encrypt/decrypt them as longed as they are scoped to them. --- diff --git a/src/creds/creds.c b/src/creds/creds.c index e097d4bf0c..4990b5967e 100644 --- a/src/creds/creds.c +++ b/src/creds/creds.c @@ -1012,12 +1012,43 @@ 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) { @@ -1026,6 +1057,50 @@ static void method_encrypt_parameters_done(MethodEncryptParameters *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[] = { @@ -1034,15 +1109,22 @@ static int vl_method_encrypt(Varlink *link, JsonVariant *parameters, VarlinkMeth { "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); @@ -1056,23 +1138,40 @@ static int vl_method_encrypt(Varlink *link, JsonVariant *parameters, VarlinkMeth /* 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) + 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 = varlink_verify_polkit_async( - link, - /* bus= */ NULL, - "io.systemd.credentials.encrypt", - /* details= */ NULL, - /* good_user= */ UID_INVALID, - polkit_registry); - if (r <= 0) + 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 overriden. */ + r = varlink_verify_polkit_async( + link, + /* bus= */ NULL, + "io.systemd.credentials.encrypt", + /* details= */ NULL, + /* good_user= */ UID_INVALID, + polkit_registry); + if (r <= 0) + return r; + } + r = encrypt_credential_and_warn( - arg_with_key, + p.scope == CREDENTIAL_USER ? _CRED_AUTO_SCOPED : _CRED_AUTO, p.name, p.timestamp, p.not_after, @@ -1080,10 +1179,12 @@ static int vl_method_encrypt(Varlink *link, JsonVariant *parameters, VarlinkMeth arg_tpm2_pcr_mask, arg_tpm2_public_key, arg_tpm2_public_key_pcr_mask, - arg_uid, + p.uid, p.text ? &IOVEC_MAKE_STRING(p.text) : &p.data, - /* flags= */ 0, + cflags, &output); + if (r == -ESRCH) + return varlink_error(link, "io.systemd.Credentials.NoSuchUser", NULL); if (r < 0) return r; @@ -1103,6 +1204,8 @@ 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) { @@ -1117,14 +1220,21 @@ static int vl_method_decrypt(Varlink *link, JsonVariant *parameters, VarlinkMeth { "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); @@ -1135,34 +1245,67 @@ static int vl_method_decrypt(Varlink *link, JsonVariant *parameters, VarlinkMeth if (p.name && !credential_name_valid(p.name)) return varlink_error_invalid_parameter_name(link, "name"); - if (p.timestamp == UINT64_MAX) + if (p.timestamp == UINT64_MAX) { p.timestamp = now(CLOCK_REALTIME); + timestamp_fresh = true; + } else + timestamp_fresh = timestamp_is_fresh(p.timestamp); - r = varlink_verify_polkit_async( - link, - /* bus= */ NULL, - "io.systemd.credentials.decrypt", - /* details= */ NULL, - /* good_user= */ UID_INVALID, - polkit_registry); - if (r <= 0) + r = settle_scope(link, &p.scope, &p.uid, &cflags, &any_scope_after_polkit); + if (r < 0) return r; - r = decrypt_credential_and_warn( - p.name, - p.timestamp, - arg_tpm2_device, - arg_tpm2_signature, - arg_uid, - &p.blob, - /* flags= */ 0, - &output); + 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, + /* good_user= */ UID_INVALID, + 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; diff --git a/src/shared/varlink-io.systemd.Credentials.c b/src/shared/varlink-io.systemd.Credentials.c index b827337eed..03db0b35a0 100644 --- a/src/shared/varlink-io.systemd.Credentials.c +++ b/src/shared/varlink-io.systemd.Credentials.c @@ -9,6 +9,8 @@ static VARLINK_DEFINE_METHOD( VARLINK_DEFINE_INPUT(data, VARLINK_STRING, VARLINK_NULLABLE), VARLINK_DEFINE_INPUT(timestamp, VARLINK_INT, VARLINK_NULLABLE), VARLINK_DEFINE_INPUT(notAfter, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(scope, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(uid, VARLINK_INT, VARLINK_NULLABLE), VARLINK_DEFINE_INPUT(allowInteractiveAuthentication, VARLINK_BOOL, VARLINK_NULLABLE), VARLINK_DEFINE_OUTPUT(blob, VARLINK_STRING, 0)); @@ -17,12 +19,16 @@ static VARLINK_DEFINE_METHOD( VARLINK_DEFINE_INPUT(name, VARLINK_STRING, VARLINK_NULLABLE), VARLINK_DEFINE_INPUT(blob, VARLINK_STRING, 0), VARLINK_DEFINE_INPUT(timestamp, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(scope, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(uid, VARLINK_INT, VARLINK_NULLABLE), VARLINK_DEFINE_INPUT(allowInteractiveAuthentication, VARLINK_BOOL, VARLINK_NULLABLE), VARLINK_DEFINE_OUTPUT(data, VARLINK_STRING, 0)); static VARLINK_DEFINE_ERROR(BadFormat); static VARLINK_DEFINE_ERROR(NameMismatch); static VARLINK_DEFINE_ERROR(TimeMismatch); +static VARLINK_DEFINE_ERROR(NoSuchUser); +static VARLINK_DEFINE_ERROR(BadScope); VARLINK_DEFINE_INTERFACE( io_systemd_Credentials, @@ -31,4 +37,6 @@ VARLINK_DEFINE_INTERFACE( &vl_method_Decrypt, &vl_error_BadFormat, &vl_error_NameMismatch, - &vl_error_TimeMismatch); + &vl_error_TimeMismatch, + &vl_error_NoSuchUser, + &vl_error_BadScope);