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) {
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[] = {
{ "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);
/* 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,
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;
const char *name;
struct iovec blob;
uint64_t timestamp;
+ CredentialScope scope;
+ uid_t uid;
} MethodDecryptParameters;
static void method_decrypt_parameters_done(MethodDecryptParameters *p) {
{ "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);
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;
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));
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,
&vl_method_Decrypt,
&vl_error_BadFormat,
&vl_error_NameMismatch,
- &vl_error_TimeMismatch);
+ &vl_error_TimeMismatch,
+ &vl_error_NoSuchUser,
+ &vl_error_BadScope);