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.
This commit is contained in:
+173
-30
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user