From 48d67957d507c17009f804ec91bf459a674edab7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 15 Jan 2024 17:36:44 +0100 Subject: [PATCH 1/8] creds-util: add a concept of "user-scoped" credentials So far credentials are a concept for system services only: to encrypt or decrypt credential you must be privileged, as only then you can access the TPM and the host key. Let's break this up a bit: let's add a "user-scoped" credential, that are specific to users. Internally this works by adding another step to the acquisition of the symmetric encryption key for the credential: if a "user-scoped" credential is used we'll generate an symmetric encryption key K as usual, but then we'll use it to calculate K' = HMAC(K, flags || uid || machine-id || username) and then use the resulting K' as encryption key instead. This basically includes the (public) user's identity in the encryption key, ensuring that only if the right user credentials are specified the correct key can be acquired. --- mime/io.systemd.xml | 3 + src/core/exec-credential.c | 6 +- src/creds/creds.c | 7 +- src/pcrlock/pcrlock.c | 1 + src/shared/creds-util.c | 201 +++++++++++++++++++++++++++++++++---- src/shared/creds-util.h | 11 +- src/shared/tpm2-util.c | 1 + src/test/test-creds.c | 26 +++-- 8 files changed, 223 insertions(+), 33 deletions(-) diff --git a/mime/io.systemd.xml b/mime/io.systemd.xml index f362006a478..8314569ed31 100644 --- a/mime/io.systemd.xml +++ b/mime/io.systemd.xml @@ -33,10 +33,13 @@ + + + diff --git a/src/core/exec-credential.c b/src/core/exec-credential.c index 41c0fce13b5..80ebd96f971 100644 --- a/src/core/exec-credential.c +++ b/src/core/exec-credential.c @@ -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; diff --git a/src/creds/creds.c b/src/creds/creds.c index bbc705c0069..a02ea2c44c6 100644 --- a/src/creds/creds.c +++ b/src/creds/creds.c @@ -428,8 +428,9 @@ static int verb_cat(int argc, char **argv, void *userdata) { timestamp, arg_tpm2_device, arg_tpm2_signature, + getuid(), &IOVEC_MAKE(data, size), - /* flags= */ 0, + CREDENTIAL_ANY_SCOPE, &plaintext); if (r < 0) return r; @@ -501,6 +502,7 @@ static int verb_encrypt(int argc, char **argv, void *userdata) { arg_tpm2_pcr_mask, arg_tpm2_public_key, arg_tpm2_public_key_pcr_mask, + /* uid= */ UID_INVALID, &plaintext, /* flags= */ 0, &output); @@ -590,6 +592,7 @@ static int verb_decrypt(int argc, char **argv, void *userdata) { timestamp, arg_tpm2_device, arg_tpm2_signature, + /* uid= */ UID_INVALID, &input, /* flags= */ 0, &plaintext); @@ -1029,6 +1032,7 @@ static int vl_method_encrypt(Varlink *link, JsonVariant *parameters, VarlinkMeth arg_tpm2_pcr_mask, arg_tpm2_public_key, arg_tpm2_public_key_pcr_mask, + /* uid= */ UID_INVALID, p.text ? &IOVEC_MAKE_STRING(p.text) : &p.data, /* flags= */ 0, &output); @@ -1101,6 +1105,7 @@ static int vl_method_decrypt(Varlink *link, JsonVariant *parameters, VarlinkMeth p.timestamp, arg_tpm2_device, arg_tpm2_signature, + /* uid= */ UID_INVALID, &p.blob, /* flags= */ 0, &output); diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c index 329153c65e4..9a9da049b27 100644 --- a/src/pcrlock/pcrlock.c +++ b/src/pcrlock/pcrlock.c @@ -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); diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index 0325f6e1293..2e9af638f72 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -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,7 @@ #include "sparse-endian.h" #include "stat-util.h" #include "tpm2-util.h" +#include "user-util.h" #define PUBLIC_KEY_MAX (UINT32_C(1024) * UINT32_C(1024)) @@ -189,10 +191,11 @@ int read_credential_with_decryption(const char *name, void **ret, size_t *ret_si r = decrypt_credential_and_warn( name, now(CLOCK_REALTIME), - /* tpm2_device = */ NULL, - /* tpm2_signature_path = */ NULL, + /* tpm2_device= */ NULL, + /* tpm2_signature_path= */ NULL, + getuid(), &IOVEC_MAKE(data, sz), - /* flags= */ 0, + CREDENTIAL_ANY_SCOPE, &ret_iovec); if (r < 0) return r; @@ -665,6 +668,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 +681,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 +739,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 +800,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 +823,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 +852,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 +885,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 +898,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 +972,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 +986,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 +1014,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 +1060,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 +1105,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 +1185,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 +1196,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 +1210,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 +1245,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 +1276,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 +1305,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 +1325,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 +1361,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 +1388,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,11 +1525,11 @@ 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."); } diff --git a/src/shared/creds-util.h b/src/shared/creds-util.h index 9362d4e52c4..bd189e8adb3 100644 --- a/src/shared/creds-util.h +++ b/src/shared/creds-util.h @@ -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,7 @@ 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); diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 713ea2a4956..bd503c5c487 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -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); diff --git a/src/test/test-creds.c b/src/test/test-creds.c index e65aa819dd5..b4beafc31d6 100644 --- a/src/test/test-creds.c +++ b/src/test/test-creds.c @@ -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, }; From c85b68f630852d38d5562ce5e2be36a22e3e7f9b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 15 Jan 2024 17:43:15 +0100 Subject: [PATCH 2/8] creds-tool: add --user/--uid= to operate with scoped credentials --- src/creds/creds.c | 58 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/src/creds/creds.c b/src/creds/creds.c index a02ea2c44c6..e097d4bf0c3 100644 --- a/src/creds/creds.c +++ b/src/creds/creds.c @@ -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); @@ -428,7 +429,7 @@ static int verb_cat(int argc, char **argv, void *userdata) { timestamp, arg_tpm2_device, arg_tpm2_signature, - getuid(), + uid_is_valid(arg_uid) ? arg_uid : getuid(), &IOVEC_MAKE(data, size), CREDENTIAL_ANY_SCOPE, &plaintext); @@ -502,7 +503,7 @@ static int verb_encrypt(int argc, char **argv, void *userdata) { arg_tpm2_pcr_mask, arg_tpm2_public_key, arg_tpm2_public_key_pcr_mask, - /* uid= */ UID_INVALID, + arg_uid, &plaintext, /* flags= */ 0, &output); @@ -592,7 +593,7 @@ static int verb_decrypt(int argc, char **argv, void *userdata) { timestamp, arg_tpm2_device, arg_tpm2_signature, - /* uid= */ UID_INVALID, + arg_uid, &input, /* flags= */ 0, &plaintext); @@ -710,6 +711,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 @@ -740,6 +743,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[] = { @@ -762,6 +767,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 }, {} }; @@ -923,6 +930,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; @@ -935,6 +968,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) @@ -1032,7 +1080,7 @@ static int vl_method_encrypt(Varlink *link, JsonVariant *parameters, VarlinkMeth arg_tpm2_pcr_mask, arg_tpm2_public_key, arg_tpm2_public_key_pcr_mask, - /* uid= */ UID_INVALID, + arg_uid, p.text ? &IOVEC_MAKE_STRING(p.text) : &p.data, /* flags= */ 0, &output); @@ -1105,7 +1153,7 @@ static int vl_method_decrypt(Varlink *link, JsonVariant *parameters, VarlinkMeth p.timestamp, arg_tpm2_device, arg_tpm2_signature, - /* uid= */ UID_INVALID, + arg_uid, &p.blob, /* flags= */ 0, &output); From 8464f7cbd652af75566017c62bec5308d1c30775 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 17 Jan 2024 21:44:01 +0100 Subject: [PATCH 3/8] 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. --- src/creds/creds.c | 203 +++++++++++++++++--- src/shared/varlink-io.systemd.Credentials.c | 10 +- 2 files changed, 182 insertions(+), 31 deletions(-) diff --git a/src/creds/creds.c b/src/creds/creds.c index e097d4bf0c3..4990b5967e3 100644 --- a/src/creds/creds.c +++ b/src/creds/creds.c @@ -1012,12 +1012,43 @@ static int creds_main(int argc, char *argv[]) { return dispatch_verb(argc, argv, verbs, NULL); } +#define TIMESTAMP_FRESH_MAX (30*USEC_PER_SEC) + +static bool timestamp_is_fresh(usec_t x) { + usec_t n = now(CLOCK_REALTIME); + + /* We'll only allow unprivileged encryption/decryption for somehwhat "fresh" timestamps */ + + if (x > n) + return x - n <= TIMESTAMP_FRESH_MAX; + else + return n - x <= TIMESTAMP_FRESH_MAX; +} + +typedef enum CredentialScope { + CREDENTIAL_SYSTEM, + CREDENTIAL_USER, + /* One day we should add more here, for example, per-app/per-service credentials */ + _CREDENTIAL_SCOPE_MAX, + _CREDENTIAL_SCOPE_INVALID = -EINVAL, +} CredentialScope; + +static const char* credential_scope_table[_CREDENTIAL_SCOPE_MAX] = { + [CREDENTIAL_SYSTEM] = "system", + [CREDENTIAL_USER] = "user", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(credential_scope, CredentialScope); +static JSON_DISPATCH_ENUM_DEFINE(dispatch_credential_scope, CredentialScope, credential_scope_from_string); + typedef struct MethodEncryptParameters { const char *name; const char *text; struct iovec data; uint64_t timestamp; uint64_t not_after; + CredentialScope scope; + uid_t uid; } MethodEncryptParameters; static void method_encrypt_parameters_done(MethodEncryptParameters *p) { @@ -1026,6 +1057,50 @@ static void method_encrypt_parameters_done(MethodEncryptParameters *p) { iovec_done_erase(&p->data); } +static int settle_scope( + Varlink *link, + CredentialScope *scope, + uid_t *uid, + CredentialFlags *flags, + bool *any_scope_after_polkit) { + + uid_t peer_uid; + int r; + + assert(link); + assert(scope); + assert(uid); + assert(flags); + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + + if (*scope < 0) { + if (uid_is_valid(*uid)) + *scope = CREDENTIAL_USER; + else { + *scope = CREDENTIAL_SYSTEM; /* When encrypting, we spit out a system credential */ + *uid = peer_uid; /* When decrypting a user credential, use this UID */ + } + + if (peer_uid == 0) + *flags |= CREDENTIAL_ANY_SCOPE; + + if (any_scope_after_polkit) + *any_scope_after_polkit = true; + } else if (*scope == CREDENTIAL_USER) { + if (!uid_is_valid(*uid)) + *uid = peer_uid; + } else { + assert(*scope == CREDENTIAL_SYSTEM); + if (uid_is_valid(*uid)) + return varlink_error_invalid_parameter_name(link, "uid"); + } + + return 0; +} + static int vl_method_encrypt(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { static const JsonDispatch dispatch_table[] = { @@ -1034,15 +1109,22 @@ static int vl_method_encrypt(Varlink *link, JsonVariant *parameters, VarlinkMeth { "data", JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(MethodEncryptParameters, data), 0 }, { "timestamp", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(MethodEncryptParameters, timestamp), 0 }, { "notAfter", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(MethodEncryptParameters, not_after), 0 }, + { "scope", JSON_VARIANT_STRING, dispatch_credential_scope, offsetof(MethodEncryptParameters, scope), 0 }, + { "uid", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uid_gid, offsetof(MethodEncryptParameters, uid), 0 }, VARLINK_DISPATCH_POLKIT_FIELD, {} }; _cleanup_(method_encrypt_parameters_done) MethodEncryptParameters p = { .timestamp = UINT64_MAX, .not_after = UINT64_MAX, + .scope = _CREDENTIAL_SCOPE_INVALID, + .uid = UID_INVALID, }; _cleanup_(iovec_done) struct iovec output = {}; Hashmap **polkit_registry = ASSERT_PTR(userdata); + CredentialFlags cflags = 0; + bool timestamp_fresh; + uid_t peer_uid; int r; assert(link); @@ -1056,23 +1138,40 @@ static int vl_method_encrypt(Varlink *link, JsonVariant *parameters, VarlinkMeth /* Specifying both or neither the text string and the binary data is not allowed */ if (!!p.text == !!p.data.iov_base) return varlink_error_invalid_parameter_name(link, "data"); - if (p.timestamp == UINT64_MAX) + if (p.timestamp == UINT64_MAX) { p.timestamp = now(CLOCK_REALTIME); + timestamp_fresh = true; + } else + timestamp_fresh = timestamp_is_fresh(p.timestamp); if (p.not_after != UINT64_MAX && p.not_after < p.timestamp) return varlink_error_invalid_parameter_name(link, "notAfter"); - r = varlink_verify_polkit_async( - link, - /* bus= */ NULL, - "io.systemd.credentials.encrypt", - /* details= */ NULL, - /* good_user= */ UID_INVALID, - polkit_registry); - if (r <= 0) + r = settle_scope(link, &p.scope, &p.uid, &cflags, /* any_scope_after_polkit= */ NULL); + if (r < 0) return r; + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + + /* Relax security requirements if peer wants to encrypt credentials for themselves */ + bool own_scope = p.scope == CREDENTIAL_USER && p.uid == peer_uid; + + if (!own_scope || !timestamp_fresh) { + /* Insist on PK if client wants to encrypt for another user or the system, or if the timestamp was explicitly overriden. */ + r = varlink_verify_polkit_async( + link, + /* bus= */ NULL, + "io.systemd.credentials.encrypt", + /* details= */ NULL, + /* good_user= */ UID_INVALID, + polkit_registry); + if (r <= 0) + return r; + } + r = encrypt_credential_and_warn( - arg_with_key, + p.scope == CREDENTIAL_USER ? _CRED_AUTO_SCOPED : _CRED_AUTO, p.name, p.timestamp, p.not_after, @@ -1080,10 +1179,12 @@ static int vl_method_encrypt(Varlink *link, JsonVariant *parameters, VarlinkMeth arg_tpm2_pcr_mask, arg_tpm2_public_key, arg_tpm2_public_key_pcr_mask, - arg_uid, + p.uid, p.text ? &IOVEC_MAKE_STRING(p.text) : &p.data, - /* flags= */ 0, + cflags, &output); + if (r == -ESRCH) + return varlink_error(link, "io.systemd.Credentials.NoSuchUser", NULL); if (r < 0) return r; @@ -1103,6 +1204,8 @@ typedef struct MethodDecryptParameters { const char *name; struct iovec blob; uint64_t timestamp; + CredentialScope scope; + uid_t uid; } MethodDecryptParameters; static void method_decrypt_parameters_done(MethodDecryptParameters *p) { @@ -1117,14 +1220,21 @@ static int vl_method_decrypt(Varlink *link, JsonVariant *parameters, VarlinkMeth { "name", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(MethodDecryptParameters, name), 0 }, { "blob", JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(MethodDecryptParameters, blob), JSON_MANDATORY }, { "timestamp", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(MethodDecryptParameters, timestamp), 0 }, + { "scope", JSON_VARIANT_STRING, dispatch_credential_scope, offsetof(MethodDecryptParameters, scope), 0 }, + { "uid", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uid_gid, offsetof(MethodDecryptParameters, uid), 0 }, VARLINK_DISPATCH_POLKIT_FIELD, {} }; _cleanup_(method_decrypt_parameters_done) MethodDecryptParameters p = { .timestamp = UINT64_MAX, + .scope = _CREDENTIAL_SCOPE_INVALID, + .uid = UID_INVALID, }; + bool timestamp_fresh, any_scope_after_polkit = false; _cleanup_(iovec_done_erase) struct iovec output = {}; Hashmap **polkit_registry = ASSERT_PTR(userdata); + CredentialFlags cflags = 0; + uid_t peer_uid; int r; assert(link); @@ -1135,34 +1245,67 @@ static int vl_method_decrypt(Varlink *link, JsonVariant *parameters, VarlinkMeth if (p.name && !credential_name_valid(p.name)) return varlink_error_invalid_parameter_name(link, "name"); - if (p.timestamp == UINT64_MAX) + if (p.timestamp == UINT64_MAX) { p.timestamp = now(CLOCK_REALTIME); + timestamp_fresh = true; + } else + timestamp_fresh = timestamp_is_fresh(p.timestamp); - r = varlink_verify_polkit_async( - link, - /* bus= */ NULL, - "io.systemd.credentials.decrypt", - /* details= */ NULL, - /* good_user= */ UID_INVALID, - polkit_registry); - if (r <= 0) + r = settle_scope(link, &p.scope, &p.uid, &cflags, &any_scope_after_polkit); + if (r < 0) return r; - r = decrypt_credential_and_warn( - p.name, - p.timestamp, - arg_tpm2_device, - arg_tpm2_signature, - arg_uid, - &p.blob, - /* flags= */ 0, - &output); + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + + /* Relax security requirements if peer wants to encrypt credentials for themselves */ + bool own_scope = p.scope == CREDENTIAL_USER && p.uid == peer_uid; + bool ask_polkit = !own_scope || !timestamp_fresh; + for (;;) { + if (ask_polkit) { + r = varlink_verify_polkit_async( + link, + /* bus= */ NULL, + "io.systemd.credentials.decrypt", + /* details= */ NULL, + /* good_user= */ UID_INVALID, + polkit_registry); + if (r <= 0) + return r; + + /* Now that we have authenticated, it's fine to allow unpriv clients access to system secrets */ + if (any_scope_after_polkit) + cflags |= CREDENTIAL_ANY_SCOPE; + } + + r = decrypt_credential_and_warn( + p.name, + p.timestamp, + arg_tpm2_device, + arg_tpm2_signature, + p.uid, + &p.blob, + cflags, + &output); + if (r != -EMEDIUMTYPE || ask_polkit || !any_scope_after_polkit) + break; + + /* So the secret was apparently intended for the system. Let's retry decrypting it after + * acquiring polkit's permission. */ + ask_polkit = true; + } + if (r == -EBADMSG) return varlink_error(link, "io.systemd.Credentials.BadFormat", NULL); if (r == -EREMOTE) return varlink_error(link, "io.systemd.Credentials.NameMismatch", NULL); if (r == -ESTALE) return varlink_error(link, "io.systemd.Credentials.TimeMismatch", NULL); + if (r == -ESRCH) + return varlink_error(link, "io.systemd.Credentials.NoSuchUser", NULL); + if (r == -EMEDIUMTYPE) + return varlink_error(link, "io.systemd.Credentials.BadScope", NULL); if (r < 0) return r; diff --git a/src/shared/varlink-io.systemd.Credentials.c b/src/shared/varlink-io.systemd.Credentials.c index b827337eedf..03db0b35a0d 100644 --- a/src/shared/varlink-io.systemd.Credentials.c +++ b/src/shared/varlink-io.systemd.Credentials.c @@ -9,6 +9,8 @@ static VARLINK_DEFINE_METHOD( VARLINK_DEFINE_INPUT(data, VARLINK_STRING, VARLINK_NULLABLE), VARLINK_DEFINE_INPUT(timestamp, VARLINK_INT, VARLINK_NULLABLE), VARLINK_DEFINE_INPUT(notAfter, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(scope, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(uid, VARLINK_INT, VARLINK_NULLABLE), VARLINK_DEFINE_INPUT(allowInteractiveAuthentication, VARLINK_BOOL, VARLINK_NULLABLE), VARLINK_DEFINE_OUTPUT(blob, VARLINK_STRING, 0)); @@ -17,12 +19,16 @@ static VARLINK_DEFINE_METHOD( VARLINK_DEFINE_INPUT(name, VARLINK_STRING, VARLINK_NULLABLE), VARLINK_DEFINE_INPUT(blob, VARLINK_STRING, 0), VARLINK_DEFINE_INPUT(timestamp, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(scope, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(uid, VARLINK_INT, VARLINK_NULLABLE), VARLINK_DEFINE_INPUT(allowInteractiveAuthentication, VARLINK_BOOL, VARLINK_NULLABLE), VARLINK_DEFINE_OUTPUT(data, VARLINK_STRING, 0)); static VARLINK_DEFINE_ERROR(BadFormat); static VARLINK_DEFINE_ERROR(NameMismatch); static VARLINK_DEFINE_ERROR(TimeMismatch); +static VARLINK_DEFINE_ERROR(NoSuchUser); +static VARLINK_DEFINE_ERROR(BadScope); VARLINK_DEFINE_INTERFACE( io_systemd_Credentials, @@ -31,4 +37,6 @@ VARLINK_DEFINE_INTERFACE( &vl_method_Decrypt, &vl_error_BadFormat, &vl_error_NameMismatch, - &vl_error_TimeMismatch); + &vl_error_TimeMismatch, + &vl_error_NoSuchUser, + &vl_error_BadScope); From 2c3cbc5c018e05b3a3e5ec6c6a2da6f3c6c3fb09 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 17 Jan 2024 21:45:12 +0100 Subject: [PATCH 4/8] creds-util: add IPC client wrapper for new varlink apis --- src/shared/creds-util.c | 128 ++++++++++++++++++++++++++++++++++++++++ src/shared/creds-util.h | 3 + 2 files changed, 131 insertions(+) diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index 2e9af638f72..c68970384f1 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -30,6 +30,7 @@ #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)) @@ -1534,3 +1535,130 @@ int decrypt_credential_and_warn(const char *validate_name, usec_t validate_times } #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; +} diff --git a/src/shared/creds-util.h b/src/shared/creds-util.h index bd189e8adb3..e56292c7958 100644 --- a/src/shared/creds-util.h +++ b/src/shared/creds-util.h @@ -90,3 +90,6 @@ typedef enum CredentialFlags { 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); From 19f16c9935c49fa93d1e94178238bf4f76c48fa3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 16 Jan 2024 11:05:22 +0100 Subject: [PATCH 5/8] creds: go via IPC service when unprivileged and trying to access services Fixes: #30191 --- src/creds/creds.c | 90 +++++++++++++++++++++++++++-------------- src/shared/creds-util.c | 27 ++++++++----- 2 files changed, 77 insertions(+), 40 deletions(-) diff --git a/src/creds/creds.c b/src/creds/creds.c index 4990b5967e3..edebe53dcae 100644 --- a/src/creds/creds.c +++ b/src/creds/creds.c @@ -424,15 +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, - uid_is_valid(arg_uid) ? arg_uid : getuid(), - &IOVEC_MAKE(data, size), - CREDENTIAL_ANY_SCOPE, - &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; @@ -494,19 +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, - arg_uid, - &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; @@ -588,15 +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, - arg_uid, - &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; diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index c68970384f1..a495f82b875 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -189,15 +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, - getuid(), - &IOVEC_MAKE(data, sz), - CREDENTIAL_ANY_SCOPE, - &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; From 6ab41e38e9ae2c3083bc9767909c673a1d18375c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 16 Jan 2024 15:22:31 +0100 Subject: [PATCH 6/8] test: add integration test for per-user creds --- test/units/testsuite-54.sh | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/units/testsuite-54.sh b/test/units/testsuite-54.sh index c7d11cffe6e..3c92994b14d 100755 --- a/test/units/testsuite-54.sh +++ b/test/units/testsuite-54.sh @@ -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 From 7704c3474d0f3176f5d84efee5f44f9d815e615f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 16 Jan 2024 16:56:12 +0100 Subject: [PATCH 7/8] man: document new user-scoped credentials --- man/systemd-creds.xml | 30 ++++++++++++++++++++++++++++++ man/systemd.exec.xml | 6 ++++++ 2 files changed, 36 insertions(+) diff --git a/man/systemd-creds.xml b/man/systemd-creds.xml index 5f52540e84e..2650dddd7ea 100644 --- a/man/systemd-creds.xml +++ b/man/systemd-creds.xml @@ -214,6 +214,36 @@ + + + + When specified with the encrypt and decrypt + commands encrypts a user-scoped (rather than a system-scoped) credential. Use + 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. + + Internally, this ensures that the selected user's numeric UID and username, as well as the + system's + machine-id5 are + incorporated into the encryption key. + + + + + + + + Specifies the user to encrypt the credential for. Takes a user name or numeric + UID. If set, implies . If set to the special string self + sets the user to the user of the calling process. If is used without + then is implied, i.e. the credential is encrypted + for the calling user. + + + + diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index 42e6ff8fd75..ca20e6e3081 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -3396,6 +3396,12 @@ StandardInputData=V2XigLJyZSBubyBzdHJhbmdlcnMgdG8gbG92ZQpZb3Uga25vdyB0aGUgcnVsZX systemd.resource-control5 for the details about DevicePolicy= or DeviceAllow=. + Note that encrypted credentials targeted for services of the per-user service manager must be + encrypted with systemd-creds encrypt --user, and those for the system service + manager without the 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. + 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 From fd40e7da6e005644445d2f6cb3363daf1e170b8c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 15 Jan 2024 15:03:09 +0100 Subject: [PATCH 8/8] update TODO --- TODO | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/TODO b/TODO index e2e6162ce82..d0d11f238b1 100644 --- a/TODO +++ b/TODO @@ -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