creds: allow Varlink clients to encrypt/decrypt their own credentials without polkit...
authorLennart Poettering <lennart@poettering.net>
Wed, 17 Jan 2024 20:44:01 +0000 (21:44 +0100)
committerLennart Poettering <lennart@poettering.net>
Tue, 30 Jan 2024 16:07:47 +0000 (17:07 +0100)
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.

src/creds/creds.c
src/shared/varlink-io.systemd.Credentials.c

index e097d4bf0c36759bc7b995252c93d1edd6267630..4990b5967e3e36b39bc7b92911f63b760a151ae3 100644 (file)
@@ -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;
 
index b827337eedf11cb396d9ce70d04147afda934e65..03db0b35a0d989ec9f7f3c9f8a9204ef0cd72ab6 100644 (file)
@@ -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);