Merge pull request #30968 from poettering/per-user-creds

per-user encrypted credentials
This commit is contained in:
Lennart Poettering 2024-01-31 09:47:12 +01:00 committed by GitHub
commit b45f47aaad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 733 additions and 112 deletions

36
TODO
View file

@ -142,6 +142,24 @@ Features:
* ditto: rewrite bpf-firewall in libbpf/C code
* credentials: if we ever acquire a secure way to derive cgroup id of socket
peers (i.e. SO_PEERCGROUPID), then extend the "scoped" credential logic to
allow cgroup-scoped (i.e. app or service scoped) credentials. Then, as next
step use this to implement per-app/per-service encrypted directories, where
we set up fscrypt on the StateDirectory= with a randomized key which is
stored as xattr on the directory, encrypted as a credential.
* credentials: optionally include a per-user secret in scoped user-credential
encryption keys. should come from homed in some way, derived from the luks
volume key or fscrypt directory key.
* credentials: add a flag to the scoped credentials that if set require PK
reauthentication when unlocking a secret.
* teach systemd --user to properly load credentials off disk, with
/etc/credstore equivalent and similar. Mkae sure that $CREDENTIALS_DIRECTORY=
actually works too when run with user privs.
* extend the smbios11 logic for passing credentials so that instead of passing
the credential data literally it can also just reference an AF_VSOCK CID/port
to read them from. This way the data doesn't remain in the SMBIOS blob during
@ -169,23 +187,11 @@ Features:
* use udev rule networkd ownership property to take ownership of network
interfaces nspawn creates
* support encrypted credentials in user context too. This is complicated by the
fact that the user does not have access to the TPM nor the system
credential. Implementation idea: extend the systemd-creds Varlink interface
to allow this: user must supply some per-user secret, that we'll include in
the encryption key.
* add a kernel cmdline switch (and cred?) for marking a system to be
"headless", in which case we never open /dev/console for reading, only for
writing. This would then mean: systemd-firstboot would process creds but not
ask interactively, getty would not be started and so on.
* extend mime database with mime types for:
- journal files
- credential files
- hwdb files
- catalog files
* cryptsetup: new crypttab option to auto-grow a luks device to its backing
partition size. new crypttab option to reencrypt a luks device with a new
volume key.
@ -689,10 +695,6 @@ Features:
- If run on every boot, should it use the sysupdate config from the host on
subsequent boots?
* provide an API (probably IPC) to apps to encrypt/decrypt
credentials. use case: allow bluez bluetooth daemon to pass pairings to initrd
that way, without shelling out to our tools.
* revisit default PCR bindings in cryptenroll and systemd-creds. Currently they
use PCR 7 which should contain secureboot state db/dbx. Which sounded like a
safe bet, given that it should change only on policy changes, and not
@ -1323,8 +1325,6 @@ Features:
wireguard)
- make gatewayd/remote read key via creds logic
- add sd_notify() command for flushing out creds not needed anymore
- make user manager instances create and use a user-specific key (the one in
/var/lib is root-only) and add --user switch to systemd-creds to use it
* TPM2: auto-reenroll in cryptsetup, as fallback for hosed firmware upgrades
and such

View file

@ -214,6 +214,36 @@
<xi:include href="version-info.xml" xpointer="v250"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--user</option></term>
<listitem><para>When specified with the <command>encrypt</command> and <command>decrypt</command>
commands encrypts a user-scoped (rather than a system-scoped) credential. Use <option>--uid=</option>
to select which user the credential is from. Such credentials may only be decrypted from the
specified user's context, except if privileges can be acquired. Generally, when an encrypted
credential shall be used in the per-user service manager it should be encrypted with this option set,
when it shall be used in the system service manager it should be encypted without.</para>
<para>Internally, this ensures that the selected user's numeric UID and username, as well as the
system's
<citerefentry><refentrytitle>machine-id</refentrytitle><manvolnum>5</manvolnum></citerefentry> are
incorporated into the encryption key.</para>
<xi:include href="version-info.xml" xpointer="v256"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--uid=</option></term>
<listitem><para>Specifies the user to encrypt the credential for. Takes a user name or numeric
UID. If set, implies <option>--user</option>. If set to the special string <literal>self</literal>
sets the user to the user of the calling process. If <option>--user</option> is used without
<option>--uid=</option> then <option>--uid=self</option> is implied, i.e. the credential is encrypted
for the calling user.</para>
<xi:include href="version-info.xml" xpointer="v256"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--transcode=</option></term>

View file

@ -3396,6 +3396,12 @@ StandardInputData=V2XigLJyZSBubyBzdHJhbmdlcnMgdG8gbG92ZQpZb3Uga25vdyB0aGUgcnVsZX
<citerefentry><refentrytitle>systemd.resource-control</refentrytitle><manvolnum>5</manvolnum></citerefentry>
for the details about <varname>DevicePolicy=</varname> or <varname>DeviceAllow=</varname>.</para>
<para>Note that encrypted credentials targeted for services of the per-user service manager must be
encrypted with <command>systemd-creds encrypt --user</command>, and those for the system service
manager without the <option>--user</option> switch. Encrypted credentials are always targeted to a
specific user or the system as a whole, and it is ensured that per-user service managers cannot
decrypt secrets intended for the system or for other users.</para>
<para>The credential files/IPC sockets must be accessible to the service manager, but don't have to
be directly accessible to the unit's processes: the credential data is read and copied into separate,
read-only copies for the unit that are accessible to appropriately privileged processes. This is

View file

@ -33,10 +33,13 @@
<generic-icon name="security-high"/>
<magic>
<match type="string" value="Whxqht+dQJax1aZeCGLxm" offset="0"/>
<match type="string" value="VbntHThZTUOoMZ0uuzMqx" offset="0"/>
<match type="string" value="DHzAexF2RZGcSwvqCLwg/" offset="0"/>
<match type="string" value="+vfrk0HjQSyhpDb5Wik2L" offset="0"/>
<match type="string" value="k6iUCUh0RJCQyvL8k8q1U" offset="0"/>
<match type="string" value="70rBNnmpSA6n22iJf58WX" offset="0"/>
<match type="string" value="r0lQqEkTTrGnOEYwT/MMB" offset="0"/>
<match type="string" value="rbxMo++2QgG6iBtvLkCV6" offset="0"/>
<match type="string" value="BYRp2vb1QySABUnaD46i+" offset="0"/>
</magic>
</mime-type>

View file

@ -281,8 +281,9 @@ static int maybe_decrypt_and_write_credential(
now(CLOCK_REALTIME),
/* tpm2_device= */ NULL,
/* tpm2_signature_path= */ NULL,
getuid(),
&IOVEC_MAKE(data, size),
/* flags= */ 0,
CREDENTIAL_ANY_SCOPE,
&plaintext);
if (r < 0)
return r;
@ -707,8 +708,9 @@ static int acquire_credentials(
now(CLOCK_REALTIME),
/* tpm2_device= */ NULL,
/* tpm2_signature_path= */ NULL,
getuid(),
&IOVEC_MAKE(sc->data, sc->size),
/* flags= */ 0,
CREDENTIAL_ANY_SCOPE,
&plaintext);
if (r < 0)
return r;

View file

@ -59,6 +59,7 @@ 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_DESTRUCTOR_REGISTER(arg_tpm2_public_key, freep);
STATIC_DESTRUCTOR_REGISTER(arg_tpm2_signature, freep);
@ -423,14 +424,24 @@ static int verb_cat(int argc, char **argv, void *userdata) {
if (encrypted) {
_cleanup_(iovec_done_erase) struct iovec plaintext = {};
r = decrypt_credential_and_warn(
*cn,
timestamp,
arg_tpm2_device,
arg_tpm2_signature,
&IOVEC_MAKE(data, size),
/* flags= */ 0,
&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;
@ -492,18 +503,29 @@ 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,
/* flags= */ 0,
&output);
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;
@ -585,14 +607,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,
/* flags= */ 0,
&plaintext);
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,
/* flags= */ 0,
&plaintext);
if (r < 0)
return r;
@ -707,6 +739,8 @@ 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"
" -q --quiet Suppress output for 'has-tpm2' verb\n"
"\nSee the %2$s for details.\n"
, program_invocation_short_name
@ -737,6 +771,8 @@ static int parse_argv(int argc, char *argv[]) {
ARG_NAME,
ARG_TIMESTAMP,
ARG_NOT_AFTER,
ARG_USER,
ARG_UID,
};
static const struct option options[] = {
@ -759,6 +795,8 @@ 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 },
{}
};
@ -920,6 +958,32 @@ 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 'q':
arg_quiet = true;
break;
@ -932,6 +996,21 @@ 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)
@ -961,12 +1040,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) {
@ -975,6 +1085,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[] = {
@ -983,15 +1137,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);
@ -1005,23 +1166,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,
@ -1029,9 +1207,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,
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;
@ -1051,6 +1232,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) {
@ -1065,14 +1248,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);
@ -1083,33 +1273,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,
&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;

View file

@ -4268,6 +4268,7 @@ static int write_boot_policy_file(const char *json_text) {
/* tpm2_hash_pcr_mask= */ 0,
/* tpm2_pubkey_path= */ NULL,
/* tpm2_pubkey_path_mask= */ 0,
UID_INVALID,
&IOVEC_MAKE_STRING(json_text),
CREDENTIAL_ALLOW_NULL,
&encoded);

View file

@ -17,6 +17,7 @@
#include "env-util.h"
#include "fd-util.h"
#include "fileio.h"
#include "format-util.h"
#include "fs-util.h"
#include "io-util.h"
#include "memory-util.h"
@ -28,6 +29,8 @@
#include "sparse-endian.h"
#include "stat-util.h"
#include "tpm2-util.h"
#include "user-util.h"
#include "varlink.h"
#define PUBLIC_KEY_MAX (UINT32_C(1024) * UINT32_C(1024))
@ -186,14 +189,24 @@ int read_credential_with_decryption(const char *name, void **ret, size_t *ret_si
if (r < 0)
return log_error_errno(r, "Failed to read encrypted credential data: %m");
r = decrypt_credential_and_warn(
name,
now(CLOCK_REALTIME),
/* tpm2_device = */ NULL,
/* tpm2_signature_path = */ NULL,
&IOVEC_MAKE(data, sz),
/* flags= */ 0,
&ret_iovec);
if (geteuid() != 0)
r = ipc_decrypt_credential(
name,
now(CLOCK_REALTIME),
getuid(),
&IOVEC_MAKE(data, sz),
CREDENTIAL_ANY_SCOPE,
&ret_iovec);
else
r = decrypt_credential_and_warn(
name,
now(CLOCK_REALTIME),
/* tpm2_device= */ NULL,
/* tpm2_signature_path= */ NULL,
getuid(),
&IOVEC_MAKE(data, sz),
CREDENTIAL_ANY_SCOPE,
&ret_iovec);
if (r < 0)
return r;
@ -665,6 +678,11 @@ struct _packed_ tpm2_public_key_credential_header {
/* Followed by NUL bytes until next 8 byte boundary */
};
struct _packed_ scoped_credential_header {
le64_t flags; /* SCOPE_HASH_DATA_BASE_FLAGS for now */
};
/* This header is encrypted */
struct _packed_ metadata_credential_header {
le64_t timestamp;
le64_t not_after;
@ -673,6 +691,23 @@ struct _packed_ metadata_credential_header {
/* Followed by NUL bytes until next 8 byte boundary */
};
struct _packed_ scoped_hash_data {
le64_t flags; /* copy of the scoped_credential_header.flags */
le32_t uid;
sd_id128_t machine_id;
char username[]; /* followed by the username */
/* Later on we might want to extend this: with a cgroup path to allow per-app secrets, and with the user's $HOME encryption key */
};
enum {
/* Flags for scoped_hash_data.flags and scoped_credential_header.flags */
SCOPE_HASH_DATA_HAS_UID = 1 << 0,
SCOPE_HASH_DATA_HAS_MACHINE = 1 << 1,
SCOPE_HASH_DATA_HAS_USERNAME = 1 << 2,
SCOPE_HASH_DATA_BASE_FLAGS = SCOPE_HASH_DATA_HAS_UID | SCOPE_HASH_DATA_HAS_USERNAME | SCOPE_HASH_DATA_HAS_MACHINE,
};
/* Some generic limit for parts of the encrypted credential for which we don't know the right size ahead of
* time, but where we are really sure it won't be larger than this. Should be larger than any possible IV,
* padding, tag size and so on. This is purely used for early filtering out of invalid sizes. */
@ -714,6 +749,58 @@ static int sha256_hash_host_and_tpm2_key(
return 0;
}
static int mangle_uid_into_key(
uid_t uid,
uint8_t md[static SHA256_DIGEST_LENGTH]) {
sd_id128_t mid;
int r;
assert(uid_is_valid(uid));
assert(md);
/* If we shall encrypt for a specific user, we HMAC() a structure with the user's credentials
* (specifically, UID, user name, machine ID) with the key we'd otherwise use for system credentials,
* and use the resulting hash as actual encryption key. */
errno = 0;
struct passwd *pw = getpwuid(uid);
if (!pw)
return log_error_errno(
IN_SET(errno, 0, ENOENT) ? SYNTHETIC_ERRNO(ESRCH) : errno,
"Failed to resolve UID " UID_FMT ": %m", uid);
r = sd_id128_get_machine(&mid);
if (r < 0)
return log_error_errno(r, "Failed to read machine ID: %m");
size_t sz = offsetof(struct scoped_hash_data, username) + strlen(pw->pw_name) + 1;
_cleanup_free_ struct scoped_hash_data *d = malloc0(sz);
if (!d)
return log_oom();
d->flags = htole64(SCOPE_HASH_DATA_BASE_FLAGS);
d->uid = htole32(uid);
d->machine_id = mid;
strcpy(d->username, pw->pw_name);
_cleanup_(erase_and_freep) void *buf = NULL;
size_t buf_size = 0;
r = openssl_hmac_many(
"sha256",
md, SHA256_DIGEST_LENGTH,
&IOVEC_MAKE(d, sz), 1,
&buf, &buf_size);
if (r < 0)
return r;
assert(buf_size == SHA256_DIGEST_LENGTH);
memcpy(md, buf, buf_size);
return 0;
}
int encrypt_credential_and_warn(
sd_id128_t with_key,
const char *name,
@ -723,6 +810,7 @@ int encrypt_credential_and_warn(
uint32_t tpm2_hash_pcr_mask,
const char *tpm2_pubkey_path,
uint32_t tpm2_pubkey_pcr_mask,
uid_t uid,
const struct iovec *input,
CredentialFlags flags,
struct iovec *ret) {
@ -745,11 +833,15 @@ int encrypt_credential_and_warn(
if (!sd_id128_in_set(with_key,
_CRED_AUTO,
_CRED_AUTO_INITRD,
_CRED_AUTO_SCOPED,
CRED_AES256_GCM_BY_HOST,
CRED_AES256_GCM_BY_HOST_SCOPED,
CRED_AES256_GCM_BY_TPM2_HMAC,
CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED,
CRED_AES256_GCM_BY_NULL))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid key type: " SD_ID128_FORMAT_STR, SD_ID128_FORMAT_VAL(with_key));
@ -770,18 +862,32 @@ int encrypt_credential_and_warn(
log_debug("Including not-after timestamp '%s' in encrypted credential.", format_timestamp(buf, sizeof(buf), not_after));
}
if (sd_id128_in_set(with_key,
_CRED_AUTO_SCOPED,
CRED_AES256_GCM_BY_HOST_SCOPED,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED)) {
if (!uid_is_valid(uid))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Scoped credential selected, but no UID specified.");
} else
uid = UID_INVALID;
if (sd_id128_in_set(with_key,
_CRED_AUTO,
_CRED_AUTO_SCOPED,
CRED_AES256_GCM_BY_HOST,
CRED_AES256_GCM_BY_HOST_SCOPED,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK)) {
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED)) {
r = get_credential_host_secret(
CREDENTIAL_SECRET_GENERATE|
CREDENTIAL_SECRET_WARN_NOT_ENCRYPTED|
(sd_id128_equal(with_key, _CRED_AUTO) ? CREDENTIAL_SECRET_FAIL_ON_TEMPORARY_FS : 0),
(sd_id128_in_set(with_key, _CRED_AUTO, _CRED_AUTO_SCOPED) ? CREDENTIAL_SECRET_FAIL_ON_TEMPORARY_FS : 0),
&host_key);
if (r == -ENOMEDIUM && sd_id128_equal(with_key, _CRED_AUTO))
if (r == -ENOMEDIUM && sd_id128_in_set(with_key, _CRED_AUTO, _CRED_AUTO_SCOPED))
log_debug_errno(r, "Credential host secret location on temporary file system, not using.");
else if (r < 0)
return log_error_errno(r, "Failed to determine local credential host secret: %m");
@ -789,7 +895,7 @@ int encrypt_credential_and_warn(
#if HAVE_TPM2
bool try_tpm2;
if (sd_id128_in_set(with_key, _CRED_AUTO, _CRED_AUTO_INITRD)) {
if (sd_id128_in_set(with_key, _CRED_AUTO, _CRED_AUTO_INITRD, _CRED_AUTO_SCOPED)) {
/* If automatic mode is selected lets see if a TPM2 it is present. If we are running in a
* container tpm2_support will detect this, and will return a different flag combination of
* TPM2_SUPPORT_FULL, effectively skipping the use of TPM2 when inside one. */
@ -802,20 +908,24 @@ int encrypt_credential_and_warn(
CRED_AES256_GCM_BY_TPM2_HMAC,
CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK);
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED);
if (try_tpm2) {
if (sd_id128_in_set(with_key,
_CRED_AUTO,
_CRED_AUTO_INITRD,
_CRED_AUTO_SCOPED,
CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK)) {
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED)) {
/* Load public key for PCR policies, if one is specified, or explicitly requested */
r = tpm2_load_pcr_public_key(tpm2_pubkey_path, &pubkey.iov_base, &pubkey.iov_len);
if (r < 0) {
if (tpm2_pubkey_path || r != -ENOENT || !sd_id128_in_set(with_key, _CRED_AUTO, _CRED_AUTO_INITRD))
if (tpm2_pubkey_path || r != -ENOENT || !sd_id128_in_set(with_key, _CRED_AUTO, _CRED_AUTO_INITRD, _CRED_AUTO_SCOPED))
return log_error_errno(r, "Failed read TPM PCR public key: %m");
log_debug_errno(r, "Failed to read TPM2 PCR public key, proceeding without: %m");
@ -872,7 +982,7 @@ int encrypt_credential_and_warn(
if (r < 0) {
if (sd_id128_equal(with_key, _CRED_AUTO_INITRD))
log_warning("TPM2 present and used, but we didn't manage to talk to it. Credential will be refused if SecureBoot is enabled.");
else if (!sd_id128_equal(with_key, _CRED_AUTO))
else if (!sd_id128_in_set(with_key, _CRED_AUTO, _CRED_AUTO_SCOPED))
return log_error_errno(r, "Failed to seal to TPM2: %m");
log_notice_errno(r, "TPM2 sealing didn't work, continuing without TPM2: %m");
@ -886,15 +996,18 @@ int encrypt_credential_and_warn(
}
#endif
if (sd_id128_in_set(with_key, _CRED_AUTO, _CRED_AUTO_INITRD)) {
if (sd_id128_in_set(with_key, _CRED_AUTO, _CRED_AUTO_INITRD, _CRED_AUTO_SCOPED)) {
/* Let's settle the key type in auto mode now. */
if (iovec_is_set(&host_key) && iovec_is_set(&tpm2_key))
id = iovec_is_set(&pubkey) ? CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK : CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC;
else if (iovec_is_set(&tpm2_key))
id = iovec_is_set(&pubkey) ? (sd_id128_equal(with_key, _CRED_AUTO_SCOPED) ?
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED : CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK)
: (sd_id128_equal(with_key, _CRED_AUTO_SCOPED) ?
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED : CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC);
else if (iovec_is_set(&tpm2_key) && !sd_id128_equal(with_key, _CRED_AUTO_SCOPED))
id = iovec_is_set(&pubkey) ? CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK : CRED_AES256_GCM_BY_TPM2_HMAC;
else if (iovec_is_set(&host_key))
id = CRED_AES256_GCM_BY_HOST;
id = sd_id128_equal(with_key, _CRED_AUTO_SCOPED) ? CRED_AES256_GCM_BY_HOST_SCOPED : CRED_AES256_GCM_BY_HOST;
else if (sd_id128_equal(with_key, _CRED_AUTO_INITRD))
id = CRED_AES256_GCM_BY_NULL;
else
@ -911,6 +1024,12 @@ int encrypt_credential_and_warn(
if (r < 0)
return r;
if (uid_is_valid(uid)) {
r = mangle_uid_into_key(uid, md);
if (r < 0)
return r;
}
assert_se(cc = EVP_aes_256_gcm());
ksz = EVP_CIPHER_key_length(cc);
@ -951,6 +1070,7 @@ int encrypt_credential_and_warn(
ALIGN8(offsetof(struct encrypted_credential_header, iv) + ivsz) +
ALIGN8(iovec_is_set(&tpm2_key) ? offsetof(struct tpm2_credential_header, policy_hash_and_blob) + tpm2_blob.iov_len + tpm2_policy_hash.iov_len : 0) +
ALIGN8(iovec_is_set(&pubkey) ? offsetof(struct tpm2_public_key_credential_header, data) + pubkey.iov_len : 0) +
ALIGN8(uid_is_valid(uid) ? sizeof(struct scoped_credential_header) : 0) +
ALIGN8(offsetof(struct metadata_credential_header, name) + strlen_ptr(name)) +
input->iov_len + 2U * (size_t) bsz +
tsz;
@ -995,7 +1115,16 @@ int encrypt_credential_and_warn(
p += ALIGN8(offsetof(struct tpm2_public_key_credential_header, data) + pubkey.iov_len);
}
/* Pass the encrypted + TPM2 header as AAD */
if (uid_is_valid(uid)) {
struct scoped_credential_header *w;
w = (struct scoped_credential_header*) ((uint8_t*) output.iov_base + p);
w->flags = htole64(SCOPE_HASH_DATA_BASE_FLAGS);
p += ALIGN8(sizeof(struct scoped_credential_header));
}
/* Pass the encrypted + TPM2 header + scoped header as AAD */
if (EVP_EncryptUpdate(context, NULL, &added, output.iov_base, p) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to write AAD data: %s",
ERR_error_string(ERR_get_error(), NULL));
@ -1066,6 +1195,7 @@ int decrypt_credential_and_warn(
usec_t validate_timestamp,
const char *tpm2_device,
const char *tpm2_signature_path,
uid_t uid,
const struct iovec *input,
CredentialFlags flags,
struct iovec *ret) {
@ -1076,7 +1206,7 @@ int decrypt_credential_and_warn(
struct encrypted_credential_header *h;
struct metadata_credential_header *m;
uint8_t md[SHA256_DIGEST_LENGTH];
bool with_tpm2, with_tpm2_pk, with_host_key, with_null;
bool with_tpm2, with_tpm2_pk, with_host_key, with_null, with_scope;
const EVP_CIPHER *cc;
size_t p, hs;
int r, added;
@ -1090,10 +1220,11 @@ int decrypt_credential_and_warn(
if (input->iov_len < sizeof(h->id))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Encrypted file too short.");
with_host_key = sd_id128_in_set(h->id, CRED_AES256_GCM_BY_HOST, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK);
with_tpm2_pk = sd_id128_in_set(h->id, CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK);
with_tpm2 = sd_id128_in_set(h->id, CRED_AES256_GCM_BY_TPM2_HMAC, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC) || with_tpm2_pk;
with_host_key = sd_id128_in_set(h->id, CRED_AES256_GCM_BY_HOST, CRED_AES256_GCM_BY_HOST_SCOPED, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED);
with_tpm2_pk = sd_id128_in_set(h->id, CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED);
with_tpm2 = sd_id128_in_set(h->id, CRED_AES256_GCM_BY_TPM2_HMAC, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED) || with_tpm2_pk;
with_null = sd_id128_equal(h->id, CRED_AES256_GCM_BY_NULL);
with_scope = sd_id128_in_set(h->id, CRED_AES256_GCM_BY_HOST_SCOPED, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED);
if (!with_host_key && !with_tpm2 && !with_null)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unknown encryption format, or corrupted data: %m");
@ -1124,6 +1255,17 @@ int decrypt_credential_and_warn(
log_debug("Credential uses fixed key for use when TPM2 is absent, and TPM2 indeed is absent. Accepting.");
}
if (with_scope) {
if (!uid_is_valid(uid))
return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Encrypted file is scoped to a user, but no user selected.");
} else {
/* Refuse to unlock system credentials if user scope is requested. */
if (uid_is_valid(uid) && !FLAGS_SET(flags, CREDENTIAL_ANY_SCOPE))
return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Encrypted file is scoped to the system, but user scope selected.");
uid = UID_INVALID;
}
/* Now we know the minimum header size */
if (input->iov_len < offsetof(struct encrypted_credential_header, iv))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Encrypted file too short.");
@ -1144,6 +1286,7 @@ int decrypt_credential_and_warn(
ALIGN8(offsetof(struct encrypted_credential_header, iv) + le32toh(h->iv_size)) +
ALIGN8(with_tpm2 ? offsetof(struct tpm2_credential_header, policy_hash_and_blob) : 0) +
ALIGN8(with_tpm2_pk ? offsetof(struct tpm2_public_key_credential_header, data) : 0) +
ALIGN8(with_scope ? sizeof(struct scoped_credential_header) : 0) +
ALIGN8(offsetof(struct metadata_credential_header, name)) +
le32toh(h->tag_size))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Encrypted file too short.");
@ -1172,6 +1315,7 @@ int decrypt_credential_and_warn(
p +
ALIGN8(offsetof(struct tpm2_credential_header, policy_hash_and_blob) + le32toh(t->blob_size) + le32toh(t->policy_hash_size)) +
ALIGN8(with_tpm2_pk ? offsetof(struct tpm2_public_key_credential_header, data) : 0) +
ALIGN8(with_scope ? sizeof(struct scoped_credential_header) : 0) +
ALIGN8(offsetof(struct metadata_credential_header, name)) +
le32toh(h->tag_size))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Encrypted file too short.");
@ -1191,6 +1335,7 @@ int decrypt_credential_and_warn(
if (input->iov_len <
p +
ALIGN8(offsetof(struct tpm2_public_key_credential_header, data) + le32toh(z->size)) +
ALIGN8(with_scope ? sizeof(struct scoped_credential_header) : 0) +
ALIGN8(offsetof(struct metadata_credential_header, name)) +
le32toh(h->tag_size))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Encrypted file too short.");
@ -1226,6 +1371,22 @@ int decrypt_credential_and_warn(
#endif
}
if (with_scope) {
struct scoped_credential_header* sh = (struct scoped_credential_header*) ((uint8_t*) input->iov_base + p);
if (le64toh(sh->flags) != SCOPE_HASH_DATA_BASE_FLAGS)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Scoped credential with unsupported flags.");
if (input->iov_len <
p +
sizeof(struct scoped_credential_header) +
ALIGN8(offsetof(struct metadata_credential_header, name)) +
le32toh(h->tag_size))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Encrypted file too short.");
p += sizeof(struct scoped_credential_header);
}
if (with_host_key) {
r = get_credential_host_secret(/* flags= */ 0, &host_key);
if (r < 0)
@ -1237,6 +1398,12 @@ int decrypt_credential_and_warn(
sha256_hash_host_and_tpm2_key(&host_key, &tpm2_key, md);
if (with_scope) {
r = mangle_uid_into_key(uid, md);
if (r < 0)
return r;
}
assert_se(cc = EVP_aes_256_gcm());
/* Make sure cipher expectations match the header */
@ -1368,12 +1535,139 @@ int get_credential_host_secret(CredentialSecretFlags flags, struct iovec *ret) {
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Support for encrypted credentials not available.");
}
int encrypt_credential_and_warn(sd_id128_t with_key, const char *name, usec_t timestamp, usec_t not_after, const char *tpm2_device, uint32_t tpm2_hash_pcr_mask, const char *tpm2_pubkey_path, uint32_t tpm2_pubkey_pcr_mask, const struct iovec *input, CredentialFlags flags, struct iovec *ret) {
int encrypt_credential_and_warn(sd_id128_t with_key, const char *name, usec_t timestamp, usec_t not_after, const char *tpm2_device, uint32_t tpm2_hash_pcr_mask, const char *tpm2_pubkey_path, uint32_t tpm2_pubkey_pcr_mask, uid_t uid, const struct iovec *input, CredentialFlags flags, struct iovec *ret) {
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Support for encrypted credentials not available.");
}
int decrypt_credential_and_warn(const char *validate_name, usec_t validate_timestamp, const char *tpm2_device, const char *tpm2_signature_path, const struct iovec *input, CredentialFlags flags, struct iovec *ret) {
int decrypt_credential_and_warn(const char *validate_name, usec_t validate_timestamp, const char *tpm2_device, const char *tpm2_signature_path, uid_t uid, const struct iovec *input, CredentialFlags flags, struct iovec *ret) {
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Support for encrypted credentials not available.");
}
#endif
int ipc_encrypt_credential(const char *name, usec_t timestamp, usec_t not_after, uid_t uid, const struct iovec *input, CredentialFlags flags, struct iovec *ret) {
_cleanup_(varlink_unrefp) Varlink *vl = NULL;
int r;
assert(input && iovec_is_valid(input));
assert(ret);
r = varlink_connect_address(&vl, "/run/systemd/io.systemd.Credentials");
if (r < 0)
return log_error_errno(r, "Failed to connect to io.systemd.Credentials: %m");
/* Mark anything we get from the service as sensitive, given that it might use a NULL cypher, at least in theory */
r = varlink_set_input_sensitive(vl);
if (r < 0)
return log_error_errno(r, "Failed to enable sensitive Varlink input: %m");
/* Create the input data blob object separately, so that we can mark it as sensitive */
_cleanup_(json_variant_unrefp) JsonVariant *jinput = NULL;
r = json_build(&jinput, JSON_BUILD_IOVEC_BASE64(input));
if (r < 0)
return log_error_errno(r, "Failed to create input object: %m");
json_variant_sensitive(jinput);
_cleanup_(json_variant_unrefp) JsonVariant *reply = NULL;
const char *error_id = NULL;
r = varlink_callb(vl,
"io.systemd.Credentials.Encrypt",
&reply,
&error_id,
JSON_BUILD_OBJECT(
JSON_BUILD_PAIR_CONDITION(name, "name", JSON_BUILD_STRING(name)),
JSON_BUILD_PAIR("data", JSON_BUILD_VARIANT(jinput)),
JSON_BUILD_PAIR_CONDITION(timestamp != USEC_INFINITY, "timestamp", JSON_BUILD_UNSIGNED(timestamp)),
JSON_BUILD_PAIR_CONDITION(not_after != USEC_INFINITY, "notAfter", JSON_BUILD_UNSIGNED(not_after)),
JSON_BUILD_PAIR_CONDITION(!FLAGS_SET(flags, CREDENTIAL_ANY_SCOPE), "scope", JSON_BUILD_STRING(uid_is_valid(uid) ? "user" : "system")),
JSON_BUILD_PAIR_CONDITION(uid_is_valid(uid), "uid", JSON_BUILD_UNSIGNED(uid))));
if (r < 0)
return log_error_errno(r, "Failed to call Encrypt() varlink call.");
if (!isempty(error_id)) {
if (streq(error_id, "io.systemd.Credentials.NoSuchUser"))
return log_error_errno(SYNTHETIC_ERRNO(ESRCH), "No such user.");
return log_error_errno(varlink_error_to_errno(error_id, reply), "Failed to encrypt: %s", error_id);
}
r = json_dispatch(
reply,
(const JsonDispatch[]) {
{ "blob", JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, PTR_TO_SIZE(ret), JSON_MANDATORY },
{},
},
JSON_LOG|JSON_ALLOW_EXTENSIONS,
/* userdata= */ NULL);
if (r < 0)
return r;
return 0;
}
int ipc_decrypt_credential(const char *validate_name, usec_t validate_timestamp, uid_t uid, const struct iovec *input, CredentialFlags flags, struct iovec *ret) {
_cleanup_(varlink_unrefp) Varlink *vl = NULL;
int r;
assert(input && iovec_is_valid(input));
assert(ret);
r = varlink_connect_address(&vl, "/run/systemd/io.systemd.Credentials");
if (r < 0)
return log_error_errno(r, "Failed to connect to io.systemd.Credentials: %m");
r = varlink_set_input_sensitive(vl);
if (r < 0)
return log_error_errno(r, "Failed to enable sensitive Varlink input: %m");
/* Create the input data blob object separately, so that we can mark it as sensitive (it's supposed
* to be encrypted, but who knows maybe it uses the NULL cypher). */
_cleanup_(json_variant_unrefp) JsonVariant *jinput = NULL;
r = json_build(&jinput, JSON_BUILD_IOVEC_BASE64(input));
if (r < 0)
return log_error_errno(r, "Failed to create input object: %m");
json_variant_sensitive(jinput);
_cleanup_(json_variant_unrefp) JsonVariant *reply = NULL;
const char *error_id = NULL;
r = varlink_callb(vl,
"io.systemd.Credentials.Decrypt",
&reply,
&error_id,
JSON_BUILD_OBJECT(
JSON_BUILD_PAIR_CONDITION(validate_name, "name", JSON_BUILD_STRING(validate_name)),
JSON_BUILD_PAIR("blob", JSON_BUILD_VARIANT(jinput)),
JSON_BUILD_PAIR_CONDITION(validate_timestamp != USEC_INFINITY, "timestamp", JSON_BUILD_UNSIGNED(validate_timestamp)),
JSON_BUILD_PAIR_CONDITION(!FLAGS_SET(flags, CREDENTIAL_ANY_SCOPE), "scope", JSON_BUILD_STRING(uid_is_valid(uid) ? "user" : "system")),
JSON_BUILD_PAIR_CONDITION(uid_is_valid(uid), "uid", JSON_BUILD_UNSIGNED(uid))));
if (r < 0)
return log_error_errno(r, "Failed to call Decrypt() varlink call.");
if (!isempty(error_id)) {
if (streq(error_id, "io.systemd.Credentials.BadFormat"))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Bad credential format.");
if (streq(error_id, "io.systemd.Credentials.NameMismatch"))
return log_error_errno(SYNTHETIC_ERRNO(EREMOTE), "Name in credential doesn't match expectations.");
if (streq(error_id, "io.systemd.Credentials.TimeMismatch"))
return log_error_errno(SYNTHETIC_ERRNO(ESTALE), "Outside of credential validity time window.");
if (streq(error_id, "io.systemd.Credentials.NoSuchUser"))
return log_error_errno(SYNTHETIC_ERRNO(ESRCH), "No such user.");
if (streq(error_id, "io.systemd.Credentials.BadScope"))
return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Scope mismtach.");
return log_error_errno(varlink_error_to_errno(error_id, reply), "Failed to decrypt: %s", error_id);
}
r = json_dispatch(
reply,
(const JsonDispatch[]) {
{ "data", JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, PTR_TO_SIZE(ret), JSON_MANDATORY },
{},
},
JSON_LOG|JSON_ALLOW_EXTENSIONS,
/* userdata= */ NULL);
if (r < 0)
return r;
return 0;
}

View file

@ -59,6 +59,7 @@ int get_credential_user_password(const char *username, char **ret_password, bool
typedef enum CredentialFlags {
CREDENTIAL_ALLOW_NULL = 1 << 0, /* allow decryption of NULL key, even if TPM is around */
CREDENTIAL_ANY_SCOPE = 1 << 1, /* allow decryption of both system and user credentials */
} CredentialFlags;
/* The four modes we support: keyed only by on-disk key, only by TPM2 HMAC key, and by the combination of
@ -66,11 +67,16 @@ typedef enum CredentialFlags {
* authenticity or confidentiality, but is still useful for integrity protection, and makes things simpler
* for us to handle). */
#define CRED_AES256_GCM_BY_HOST SD_ID128_MAKE(5a,1c,6a,86,df,9d,40,96,b1,d5,a6,5e,08,62,f1,9a)
#define CRED_AES256_GCM_BY_HOST_SCOPED SD_ID128_MAKE(55,b9,ed,1d,38,59,4d,43,a8,31,9d,2e,bb,33,2a,c6)
#define CRED_AES256_GCM_BY_TPM2_HMAC SD_ID128_MAKE(0c,7c,c0,7b,11,76,45,91,9c,4b,0b,ea,08,bc,20,fe)
#define CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK SD_ID128_MAKE(fa,f7,eb,93,41,e3,41,2c,a1,a4,36,f9,5a,29,36,2f)
#define CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC SD_ID128_MAKE(93,a8,94,09,48,74,44,90,90,ca,f2,fc,93,ca,b5,53)
#define CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED \
SD_ID128_MAKE(ef,4a,c1,36,79,a9,48,0e,a7,db,68,89,7f,9f,16,5d)
#define CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK \
SD_ID128_MAKE(af,49,50,a8,49,13,4e,b1,a7,38,46,30,4f,f3,0c,05)
#define CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED \
SD_ID128_MAKE(ad,bc,4c,a3,ef,b6,42,01,ba,88,1b,6f,2e,40,95,ea)
#define CRED_AES256_GCM_BY_NULL SD_ID128_MAKE(05,84,69,da,f6,f5,43,24,80,05,49,da,0f,8e,a2,fb)
/* Two special IDs to pick a general automatic mode (i.e. tpm2+host if TPM2 exists, only host otherwise) or
@ -80,6 +86,10 @@ typedef enum CredentialFlags {
* with an underscore. */
#define _CRED_AUTO SD_ID128_MAKE(a2,19,cb,07,85,b2,4c,04,b1,6d,18,ca,b9,d2,ee,01)
#define _CRED_AUTO_INITRD SD_ID128_MAKE(02,dc,8e,de,3a,02,43,ab,a9,ec,54,9c,05,e6,a0,71)
#define _CRED_AUTO_SCOPED SD_ID128_MAKE(23,88,96,85,6f,74,48,8a,9c,78,6f,6a,b0,e7,3b,6a)
int encrypt_credential_and_warn(sd_id128_t with_key, const char *name, usec_t timestamp, usec_t not_after, const char *tpm2_device, uint32_t tpm2_hash_pcr_mask, const char *tpm2_pubkey_path, uint32_t tpm2_pubkey_pcr_mask, const struct iovec *input, CredentialFlags flags, struct iovec *ret);
int decrypt_credential_and_warn(const char *validate_name, usec_t validate_timestamp, const char *tpm2_device, const char *tpm2_signature_path, const struct iovec *input, CredentialFlags flags, struct iovec *ret);
int encrypt_credential_and_warn(sd_id128_t with_key, const char *name, usec_t timestamp, usec_t not_after, const char *tpm2_device, uint32_t tpm2_hash_pcr_mask, const char *tpm2_pubkey_path, uint32_t tpm2_pubkey_pcr_mask, uid_t uid, const struct iovec *input, CredentialFlags flags, struct iovec *ret);
int decrypt_credential_and_warn(const char *validate_name, usec_t validate_timestamp, const char *tpm2_device, const char *tpm2_signature_path, uid_t uid, const struct iovec *input, CredentialFlags flags, struct iovec *ret);
int ipc_encrypt_credential(const char *name, usec_t timestamp, usec_t not_after, uid_t uid, const struct iovec *input, CredentialFlags flags, struct iovec *ret);
int ipc_decrypt_credential(const char *validate_name, usec_t validate_timestamp, uid_t uid, const struct iovec *input, CredentialFlags flags, struct iovec *ret);

View file

@ -6891,6 +6891,7 @@ static int pcrlock_policy_load_credential(
now(CLOCK_REALTIME),
/* tpm2_device= */ NULL,
/* tpm2_signature_path= */ NULL,
UID_INVALID,
data,
CREDENTIAL_ALLOW_NULL,
&decoded);

View 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);

View file

@ -11,6 +11,7 @@
#include "tests.h"
#include "tmpfile-util.h"
#include "tpm2-util.h"
#include "user-util.h"
TEST(read_credential_strings) {
_cleanup_free_ char *x = NULL, *y = NULL, *saved = NULL, *p = NULL;
@ -119,11 +120,14 @@ TEST(credential_glob_valid) {
assert_se(credential_glob_valid(buf));
}
static void test_encrypt_decrypt_with(sd_id128_t mode) {
static void test_encrypt_decrypt_with(sd_id128_t mode, uid_t uid) {
static const struct iovec plaintext = CONST_IOVEC_MAKE_STRING("this is a super secret string");
int r;
log_notice("Running encryption/decryption test with mode " SD_ID128_FORMAT_STR ".", SD_ID128_FORMAT_VAL(mode));
if (uid_is_valid(uid))
log_notice("Running encryption/decryption test with mode " SD_ID128_FORMAT_STR " for UID " UID_FMT ".", SD_ID128_FORMAT_VAL(mode), uid);
else
log_notice("Running encryption/decryption test with mode " SD_ID128_FORMAT_STR ".", SD_ID128_FORMAT_VAL(mode));
_cleanup_(iovec_done) struct iovec encrypted = {};
r = encrypt_credential_and_warn(
@ -135,6 +139,7 @@ static void test_encrypt_decrypt_with(sd_id128_t mode) {
/* tpm2_hash_pcr_mask= */ 0,
/* tpm2_pubkey_path= */ NULL,
/* tpm2_pubkey_pcr_mask= */ 0,
uid,
&plaintext,
CREDENTIAL_ALLOW_NULL,
&encrypted);
@ -155,6 +160,7 @@ static void test_encrypt_decrypt_with(sd_id128_t mode) {
/* validate_timestamp= */ USEC_INFINITY,
/* tpm2_device= */ NULL,
/* tpm2_signature_path= */ NULL,
uid,
&encrypted,
CREDENTIAL_ALLOW_NULL,
&decrypted);
@ -165,6 +171,7 @@ static void test_encrypt_decrypt_with(sd_id128_t mode) {
/* validate_timestamp= */ USEC_INFINITY,
/* tpm2_device= */ NULL,
/* tpm2_signature_path= */ NULL,
uid,
&encrypted,
CREDENTIAL_ALLOW_NULL,
&decrypted);
@ -192,7 +199,9 @@ TEST(credential_encrypt_decrypt) {
_cleanup_(rm_rf_physical_and_freep) char *d = NULL;
_cleanup_free_ char *j = NULL;
test_encrypt_decrypt_with(CRED_AES256_GCM_BY_NULL);
log_set_max_level(LOG_DEBUG);
test_encrypt_decrypt_with(CRED_AES256_GCM_BY_NULL, UID_INVALID);
assert_se(mkdtemp_malloc(NULL, &d) >= 0);
j = path_join(d, "secret");
@ -206,11 +215,13 @@ TEST(credential_encrypt_decrypt) {
assert_se(setenv("SYSTEMD_CREDENTIAL_SECRET", j, true) >= 0);
test_encrypt_decrypt_with(CRED_AES256_GCM_BY_HOST);
test_encrypt_decrypt_with(CRED_AES256_GCM_BY_HOST, UID_INVALID);
test_encrypt_decrypt_with(CRED_AES256_GCM_BY_HOST_SCOPED, 0);
if (try_tpm2()) {
test_encrypt_decrypt_with(CRED_AES256_GCM_BY_TPM2_HMAC);
test_encrypt_decrypt_with(CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC);
test_encrypt_decrypt_with(CRED_AES256_GCM_BY_TPM2_HMAC, UID_INVALID);
test_encrypt_decrypt_with(CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC, UID_INVALID);
test_encrypt_decrypt_with(CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED, 0);
}
if (ec)
@ -221,10 +232,13 @@ TEST(mime_type_matches) {
static const sd_id128_t tags[] = {
CRED_AES256_GCM_BY_HOST,
CRED_AES256_GCM_BY_HOST_SCOPED,
CRED_AES256_GCM_BY_TPM2_HMAC,
CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK,
CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED,
CRED_AES256_GCM_BY_NULL,
};

View file

@ -324,6 +324,34 @@ varlinkctl call /run/systemd/io.systemd.Credentials io.systemd.Credentials.Encry
cmp /tmp/vlcredsdata /tmp/vlcredsdata2
rm /tmp/vlcredsdata /tmp/vlcredsdata2
clean_usertest() {
rm -f /tmp/usertest.data /tmp/usertest.data
}
trap clean_usertest EXIT
dd if=/dev/urandom of=/tmp/usertest.data bs=4096 count=1
systemd-creds encrypt --user /tmp/usertest.data /tmp/usertest.cred
systemd-creds decrypt --user /tmp/usertest.cred - | cmp /tmp/usertest.data
# Decryption must fail if it's not done in user context
(! systemd-creds decrypt /tmp/usertest.cred - )
# Decryption must also fail if a different user is used
(! systemd-creds decrypt --user --uid=65534 /tmp/usertest.cred - )
# Try the reverse
systemd-creds encrypt --user --uid=65534 /tmp/usertest.data /tmp/usertest.cred
(! systemd-creds decrypt --user /tmp/usertest.cred - )
systemd-creds decrypt --user --uid=65534 /tmp/usertest.cred - | cmp /tmp/usertest.data
systemd-creds encrypt --user /tmp/usertest.data /tmp/usertest.creds --name=mytest
# Make sure we actually can decode this in user context
systemctl start user@0.service
XDG_RUNTIME_DIR=/run/user/0 systemd-run --pipe --user --unit=waldi.service -p LoadCredentialEncrypted=mytest:/tmp/usertest.creds cat /run/user/0/credentials/waldi.service/mytest | cmp /tmp/usertest.data
systemd-analyze log-level info
touch /testok