diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 03:50:40 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 03:50:40 +0000 |
commit | fc53809803cd2bc2434e312b19a18fa36776da12 (patch) | |
tree | b4b43bd6538f51965ce32856e9c053d0f90919c8 /src/shared/creds-util.c | |
parent | Adding upstream version 255.5. (diff) | |
download | systemd-fc53809803cd2bc2434e312b19a18fa36776da12.tar.xz systemd-fc53809803cd2bc2434e312b19a18fa36776da12.zip |
Adding upstream version 256.upstream/256
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/shared/creds-util.c | 826 |
1 files changed, 587 insertions, 239 deletions
diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index fa8ebe0..1d8bd91 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -12,23 +12,28 @@ #include "capability-util.h" #include "chattr-util.h" #include "constants.h" +#include "copy.h" #include "creds-util.h" #include "efi-api.h" #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" -#include "mkdir.h" +#include "mkdir-label.h" #include "openssl-util.h" #include "parse-util.h" #include "path-util.h" #include "random-util.h" +#include "recurse-dir.h" #include "sparse-endian.h" #include "stat-util.h" +#include "tmpfile-util.h" #include "tpm2-util.h" -#include "virt.h" +#include "user-util.h" +#include "varlink.h" #define PUBLIC_KEY_MAX (UINT32_C(1024) * UINT32_C(1024)) @@ -100,6 +105,17 @@ int get_encrypted_credentials_dir(const char **ret) { return get_credentials_dir_internal("ENCRYPTED_CREDENTIALS_DIRECTORY", ret); } +int open_credentials_dir(void) { + const char *d; + int r; + + r = get_credentials_dir(&d); + if (r < 0) + return r; + + return RET_NERRNO(open(d, O_CLOEXEC|O_DIRECTORY)); +} + int read_credential(const char *name, void **ret, size_t *ret_size) { _cleanup_free_ char *fn = NULL; const char *d; @@ -127,14 +143,13 @@ int read_credential(const char *name, void **ret, size_t *ret_size) { } int read_credential_with_decryption(const char *name, void **ret, size_t *ret_size) { + _cleanup_(iovec_done_erase) struct iovec ret_iovec = {}; _cleanup_(erase_and_freep) void *data = NULL; _cleanup_free_ char *fn = NULL; size_t sz = 0; const char *d; int r; - assert(ret); - /* Just like read_credential() but will also look for encrypted credentials. Note that services only * receive decrypted credentials, hence use read_credential() for those. This helper here is for * generators, i.e. code that runs outside of service context, and thus has no decrypted credentials @@ -177,23 +192,37 @@ 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, - data, - sz, - ret, - ret_size); + 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; + if (ret) + *ret = TAKE_PTR(ret_iovec.iov_base); + if (ret_size) + *ret_size = ret_iovec.iov_len; + return 1; /* found */ not_found: - *ret = NULL; - + if (ret) + *ret = NULL; if (ret_size) *ret_size = 0; @@ -205,6 +234,7 @@ int read_credential_strings_many_internal( ...) { _cleanup_free_ void *b = NULL; + bool all = true; int r, ret = 0; /* Reads a bunch of credentials into the specified buffers. If the specified buffers are already @@ -220,10 +250,11 @@ int read_credential_strings_many_internal( r = read_credential(first_name, &b, NULL); if (r == -ENXIO) /* No creds passed at all? Bail immediately. */ return 0; - if (r < 0) { - if (r != -ENOENT) - ret = r; - } else + if (r == -ENOENT) + all = false; + else if (r < 0) + RET_GATHER(ret, r); + else free_and_replace(*first_value, b); va_list ap; @@ -238,20 +269,19 @@ int read_credential_strings_many_internal( if (!name) break; - value = va_arg(ap, char **); - if (*value) - continue; + value = ASSERT_PTR(va_arg(ap, char **)); r = read_credential(name, &bb, NULL); - if (r < 0) { - if (ret >= 0 && r != -ENOENT) - ret = r; - } else + if (r == -ENOENT) + all = false; + else if (r < 0) + RET_GATHER(ret, r); + else free_and_replace(*value, bb); } va_end(ap); - return ret; + return ret < 0 ? ret : all; } int read_credential_bool(const char *name) { @@ -341,8 +371,7 @@ static int make_credential_host_secret( CredentialSecretFlags flags, const char *dirname, const char *fn, - void **ret_data, - size_t *ret_size) { + struct iovec *ret) { _cleanup_free_ char *t = NULL; _cleanup_close_ int fd = -EBADF; @@ -351,22 +380,9 @@ static int make_credential_host_secret( assert(dfd >= 0); assert(fn); - /* For non-root users creating a temporary file using the openat(2) over "." will fail later, in the - * linkat(2) step at the end. The reason is that linkat(2) requires the CAP_DAC_READ_SEARCH - * capability when it uses the AT_EMPTY_PATH flag. */ - if (have_effective_cap(CAP_DAC_READ_SEARCH) > 0) { - fd = openat(dfd, ".", O_CLOEXEC|O_WRONLY|O_TMPFILE, 0400); - if (fd < 0) - log_debug_errno(errno, "Failed to create temporary credential file with O_TMPFILE, proceeding without: %m"); - } - if (fd < 0) { - if (asprintf(&t, "credential.secret.%016" PRIx64, random_u64()) < 0) - return -ENOMEM; - - fd = openat(dfd, t, O_CLOEXEC|O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW, 0400); - if (fd < 0) - return -errno; - } + fd = open_tmpfile_linkable_at(dfd, fn, O_CLOEXEC|O_WRONLY, &t); + if (fd < 0) + return log_debug_errno(fd, "Failed to create temporary file for credential host secret: %m"); r = chattr_secret(fd, 0); if (r < 0) @@ -386,44 +402,34 @@ static int make_credential_host_secret( if (r < 0) goto fail; - if (fsync(fd) < 0) { + if (fchmod(fd, 0400) < 0) { r = -errno; goto fail; } - warn_not_encrypted(fd, flags, dirname, fn); - - if (t) { - r = rename_noreplace(dfd, t, dfd, fn); - if (r < 0) - goto fail; - - t = mfree(t); - } else if (linkat(fd, "", dfd, fn, AT_EMPTY_PATH) < 0) { + if (fsync(fd) < 0) { r = -errno; goto fail; } - if (fsync(dfd) < 0) { - r = -errno; + warn_not_encrypted(fd, flags, dirname, fn); + + r = link_tmpfile_at(fd, dfd, t, fn, LINK_TMPFILE_SYNC); + if (r < 0) { + log_debug_errno(r, "Failed to link host key into place: %m"); goto fail; } - if (ret_data) { + if (ret) { void *copy; copy = memdup(buf.data, sizeof(buf.data)); - if (!copy) { - r = -ENOMEM; - goto fail; - } + if (!copy) + return -ENOMEM; - *ret_data = copy; + *ret = IOVEC_MAKE(copy, sizeof(buf.data)); } - if (ret_size) - *ret_size = sizeof(buf.data); - return 0; fail: @@ -433,7 +439,7 @@ fail: return r; } -int get_credential_host_secret(CredentialSecretFlags flags, void **ret, size_t *ret_size) { +int get_credential_host_secret(CredentialSecretFlags flags, struct iovec *ret) { _cleanup_free_ char *_dirname = NULL, *_filename = NULL; _cleanup_close_ int dfd = -EBADF; sd_id128_t machine_id; @@ -501,7 +507,7 @@ int get_credential_host_secret(CredentialSecretFlags flags, void **ret, size_t * "Failed to open %s/%s: %m", dirname, filename); - r = make_credential_host_secret(dfd, machine_id, flags, dirname, filename, ret, ret_size); + r = make_credential_host_secret(dfd, machine_id, flags, dirname, filename, ret); if (r == -EEXIST) { log_debug_errno(r, "Credential secret %s/%s appeared while we were creating it, rereading.", dirname, filename); @@ -549,7 +555,7 @@ int get_credential_host_secret(CredentialSecretFlags flags, void **ret, size_t * "Failed to read %s/%s: %m", dirname, filename); if ((size_t) n != l) /* What? The size changed? */ return log_debug_errno(SYNTHETIC_ERRNO(EIO), - "Failed to read %s/%s: %m", dirname, filename); + "Failed to read %s/%s.", dirname, filename); if (sd_id128_equal(machine_id, f->machine_id)) { size_t sz; @@ -568,12 +574,9 @@ int get_credential_host_secret(CredentialSecretFlags flags, void **ret, size_t * if (!copy) return log_oom_debug(); - *ret = copy; + *ret = IOVEC_MAKE(copy, sz); } - if (ret_size) - *ret_size = sz; - return 0; } @@ -658,6 +661,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; @@ -666,23 +674,38 @@ 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. */ #define CREDENTIAL_FIELD_SIZE_MAX (16U*1024U) static int sha256_hash_host_and_tpm2_key( - const void *host_key, - size_t host_key_size, - const void *tpm2_key, - size_t tpm2_key_size, + const struct iovec *host_key, + const struct iovec *tpm2_key, uint8_t ret[static SHA256_DIGEST_LENGTH]) { _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *md = NULL; unsigned l; - assert(host_key_size == 0 || host_key); - assert(tpm2_key_size == 0 || tpm2_key); + assert(iovec_is_valid(host_key)); + assert(iovec_is_valid(tpm2_key)); assert(ret); /* Combines the host key and the TPM2 HMAC hash into a SHA256 hash value we'll use as symmetric encryption key. */ @@ -694,10 +717,10 @@ static int sha256_hash_host_and_tpm2_key( if (EVP_DigestInit_ex(md, EVP_sha256(), NULL) != 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initial SHA256 context."); - if (host_key && EVP_DigestUpdate(md, host_key, host_key_size) != 1) + if (iovec_is_set(host_key) && EVP_DigestUpdate(md, host_key->iov_base, host_key->iov_len) != 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to hash host key."); - if (tpm2_key && EVP_DigestUpdate(md, tpm2_key, tpm2_key_size) != 1) + if (iovec_is_set(tpm2_key) && EVP_DigestUpdate(md, tpm2_key->iov_base, tpm2_key->iov_len) != 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to hash TPM2 key."); assert(EVP_MD_CTX_size(md) == SHA256_DIGEST_LENGTH); @@ -709,6 +732,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, @@ -718,38 +793,39 @@ int encrypt_credential_and_warn( uint32_t tpm2_hash_pcr_mask, const char *tpm2_pubkey_path, uint32_t tpm2_pubkey_pcr_mask, - const void *input, - size_t input_size, - void **ret, - size_t *ret_size) { + uid_t uid, + const struct iovec *input, + CredentialFlags flags, + struct iovec *ret) { + _cleanup_(iovec_done) struct iovec tpm2_blob = {}, tpm2_policy_hash = {}, iv = {}, pubkey = {}; + _cleanup_(iovec_done_erase) struct iovec tpm2_key = {}, output = {}, host_key = {}; _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL; - _cleanup_(erase_and_freep) void *host_key = NULL, *tpm2_key = NULL; - size_t host_key_size = 0, tpm2_key_size = 0, tpm2_blob_size = 0, tpm2_policy_hash_size = 0, output_size, p, ml; - _cleanup_free_ void *tpm2_blob = NULL, *tpm2_policy_hash = NULL, *iv = NULL, *output = NULL; _cleanup_free_ struct metadata_credential_header *m = NULL; uint16_t tpm2_pcr_bank = 0, tpm2_primary_alg = 0; struct encrypted_credential_header *h; int ksz, bsz, ivsz, tsz, added, r; - _cleanup_free_ void *pubkey = NULL; - size_t pubkey_size = 0; uint8_t md[SHA256_DIGEST_LENGTH]; const EVP_CIPHER *cc; sd_id128_t id; + size_t p, ml; - assert(input || input_size == 0); + assert(iovec_is_valid(input)); assert(ret); - assert(ret_size); 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_TPM2_ABSENT)) + 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)); if (name && !credential_name_valid(name)) @@ -770,18 +846,31 @@ int encrypt_credential_and_warn( } 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), - &host_key, - &host_key_size); - if (r == -ENOMEDIUM && sd_id128_equal(with_key, _CRED_AUTO)) + (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_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 +878,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,27 +891,31 @@ 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, &pubkey_size); + 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"); } } - if (!pubkey) + if (!iovec_is_set(&pubkey)) tpm2_pubkey_pcr_mask = 0; _cleanup_(tpm2_context_unrefp) Tpm2Context *tpm2_context = NULL; @@ -844,8 +937,8 @@ int encrypt_credential_and_warn( return log_error_errno(r, "Could not read PCR values: %m"); TPM2B_PUBLIC public; - if (pubkey) { - r = tpm2_tpm2b_public_from_pem(pubkey, pubkey_size, &public); + if (iovec_is_set(&pubkey)) { + r = tpm2_tpm2b_public_from_pem(pubkey.iov_base, pubkey.iov_len, &public); if (r < 0) return log_error_errno(r, "Could not convert public key to TPM2B_PUBLIC: %m"); } @@ -854,7 +947,7 @@ int encrypt_credential_and_warn( r = tpm2_calculate_sealing_policy( tpm2_hash_pcr_values, tpm2_n_hash_pcr_values, - pubkey ? &public : NULL, + iovec_is_set(&pubkey) ? &public : NULL, /* use_pin= */ false, /* pcrlock_policy= */ NULL, &tpm2_policy); @@ -865,56 +958,61 @@ int encrypt_credential_and_warn( /* seal_key_handle= */ 0, &tpm2_policy, /* pin= */ NULL, - &tpm2_key, &tpm2_key_size, - &tpm2_blob, &tpm2_blob_size, + &tpm2_key, + &tpm2_blob, &tpm2_primary_alg, - /* ret_srk_buf= */ NULL, - /* ret_srk_buf_size= */ NULL); + /* ret_srk= */ NULL); 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"); } - tpm2_policy_hash_size = tpm2_policy.size; - tpm2_policy_hash = malloc(tpm2_policy_hash_size); - if (!tpm2_policy_hash) + if (!iovec_memdup(&IOVEC_MAKE(tpm2_policy.buffer, tpm2_policy.size), &tpm2_policy_hash)) return log_oom(); - memcpy(tpm2_policy_hash, tpm2_policy.buffer, tpm2_policy_hash_size); - assert(tpm2_blob_size <= CREDENTIAL_FIELD_SIZE_MAX); - assert(tpm2_policy_hash_size <= CREDENTIAL_FIELD_SIZE_MAX); + assert(tpm2_blob.iov_len <= CREDENTIAL_FIELD_SIZE_MAX); + assert(tpm2_policy_hash.iov_len <= CREDENTIAL_FIELD_SIZE_MAX); } #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 (host_key && tpm2_key) - id = pubkey ? CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK : CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC; - else if (tpm2_key) - id = pubkey ? CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK : CRED_AES256_GCM_BY_TPM2_HMAC; - else if (host_key) - id = CRED_AES256_GCM_BY_HOST; + if (iovec_is_set(&host_key) && 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 = 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_TPM2_ABSENT; + id = CRED_AES256_GCM_BY_NULL; else return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "TPM2 not available and host key located on temporary file system, no encryption key available."); } else id = with_key; - if (sd_id128_equal(id, CRED_AES256_GCM_BY_TPM2_ABSENT)) + if (sd_id128_equal(id, CRED_AES256_GCM_BY_NULL) && !FLAGS_SET(flags, CREDENTIAL_ALLOW_NULL)) log_warning("Using a null key for encryption and signing. Confidentiality or authenticity will not be provided."); /* Let's now take the host key and the TPM2 key and hash it together, to use as encryption key for the data */ - r = sha256_hash_host_and_tpm2_key(host_key, host_key_size, tpm2_key, tpm2_key_size, md); + r = sha256_hash_host_and_tpm2_key(&host_key, &tpm2_key, md); 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); @@ -928,11 +1026,13 @@ int encrypt_credential_and_warn( if (ivsz > 0) { assert((size_t) ivsz <= CREDENTIAL_FIELD_SIZE_MAX); - iv = malloc(ivsz); - if (!iv) + iv.iov_base = malloc(ivsz); + if (!iv.iov_base) return log_oom(); - r = crypto_random_bytes(iv, ivsz); + iv.iov_len = ivsz; + + r = crypto_random_bytes(iv.iov_base, iv.iov_len); if (r < 0) return log_error_errno(r, "Failed to acquired randomized IV: %m"); } @@ -944,61 +1044,71 @@ int encrypt_credential_and_warn( return log_error_errno(SYNTHETIC_ERRNO(ENOMEM), "Failed to allocate encryption object: %s", ERR_error_string(ERR_get_error(), NULL)); - if (EVP_EncryptInit_ex(context, cc, NULL, md, iv) != 1) + if (EVP_EncryptInit_ex(context, cc, NULL, md, iv.iov_base) != 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize encryption context: %s", ERR_error_string(ERR_get_error(), NULL)); /* Just an upper estimate */ - output_size = + output.iov_len = ALIGN8(offsetof(struct encrypted_credential_header, iv) + ivsz) + - ALIGN8(tpm2_key ? offsetof(struct tpm2_credential_header, policy_hash_and_blob) + tpm2_blob_size + tpm2_policy_hash_size : 0) + - ALIGN8(pubkey ? offsetof(struct tpm2_public_key_credential_header, data) + pubkey_size : 0) + + 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_size + 2U * (size_t) bsz + + input->iov_len + 2U * (size_t) bsz + tsz; - output = malloc0(output_size); - if (!output) + output.iov_base = malloc0(output.iov_len); + if (!output.iov_base) return log_oom(); - h = (struct encrypted_credential_header*) output; + h = (struct encrypted_credential_header*) output.iov_base; h->id = id; h->block_size = htole32(bsz); h->key_size = htole32(ksz); h->tag_size = htole32(tsz); h->iv_size = htole32(ivsz); - memcpy(h->iv, iv, ivsz); + memcpy(h->iv, iv.iov_base, ivsz); p = ALIGN8(offsetof(struct encrypted_credential_header, iv) + ivsz); - if (tpm2_key) { + if (iovec_is_set(&tpm2_key)) { struct tpm2_credential_header *t; - t = (struct tpm2_credential_header*) ((uint8_t*) output + p); + t = (struct tpm2_credential_header*) ((uint8_t*) output.iov_base + p); t->pcr_mask = htole64(tpm2_hash_pcr_mask); t->pcr_bank = htole16(tpm2_pcr_bank); t->primary_alg = htole16(tpm2_primary_alg); - t->blob_size = htole32(tpm2_blob_size); - t->policy_hash_size = htole32(tpm2_policy_hash_size); - memcpy(t->policy_hash_and_blob, tpm2_blob, tpm2_blob_size); - memcpy(t->policy_hash_and_blob + tpm2_blob_size, tpm2_policy_hash, tpm2_policy_hash_size); + t->blob_size = htole32(tpm2_blob.iov_len); + t->policy_hash_size = htole32(tpm2_policy_hash.iov_len); + memcpy(t->policy_hash_and_blob, tpm2_blob.iov_base, tpm2_blob.iov_len); + memcpy(t->policy_hash_and_blob + tpm2_blob.iov_len, tpm2_policy_hash.iov_base, tpm2_policy_hash.iov_len); - p += ALIGN8(offsetof(struct tpm2_credential_header, policy_hash_and_blob) + tpm2_blob_size + tpm2_policy_hash_size); + p += ALIGN8(offsetof(struct tpm2_credential_header, policy_hash_and_blob) + tpm2_blob.iov_len + tpm2_policy_hash.iov_len); } - if (pubkey) { + if (iovec_is_set(&pubkey)) { struct tpm2_public_key_credential_header *z; - z = (struct tpm2_public_key_credential_header*) ((uint8_t*) output + p); + z = (struct tpm2_public_key_credential_header*) ((uint8_t*) output.iov_base + p); z->pcr_mask = htole64(tpm2_pubkey_pcr_mask); - z->size = htole32(pubkey_size); - memcpy(z->data, pubkey, pubkey_size); + z->size = htole32(pubkey.iov_len); + memcpy(z->data, pubkey.iov_base, pubkey.iov_len); + + p += ALIGN8(offsetof(struct tpm2_public_key_credential_header, data) + pubkey.iov_len); + } + + 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(offsetof(struct tpm2_public_key_credential_header, data) + pubkey_size); + p += ALIGN8(sizeof(struct scoped_credential_header)); } - /* Pass the encrypted + TPM2 header as AAD */ - if (EVP_EncryptUpdate(context, NULL, &added, output, p) != 1) + /* 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)); @@ -1014,53 +1124,52 @@ int encrypt_credential_and_warn( memcpy_safe(m->name, name, ml); /* And encrypt the metadata header */ - if (EVP_EncryptUpdate(context, (uint8_t*) output + p, &added, (const unsigned char*) m, ALIGN8(offsetof(struct metadata_credential_header, name) + ml)) != 1) + if (EVP_EncryptUpdate(context, (uint8_t*) output.iov_base + p, &added, (const unsigned char*) m, ALIGN8(offsetof(struct metadata_credential_header, name) + ml)) != 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to encrypt metadata header: %s", ERR_error_string(ERR_get_error(), NULL)); assert(added >= 0); - assert((size_t) added <= output_size - p); + assert((size_t) added <= output.iov_len - p); p += added; /* Then encrypt the plaintext */ - if (EVP_EncryptUpdate(context, (uint8_t*) output + p, &added, input, input_size) != 1) + if (EVP_EncryptUpdate(context, (uint8_t*) output.iov_base + p, &added, input->iov_base, input->iov_len) != 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to encrypt data: %s", ERR_error_string(ERR_get_error(), NULL)); assert(added >= 0); - assert((size_t) added <= output_size - p); + assert((size_t) added <= output.iov_len - p); p += added; /* Finalize */ - if (EVP_EncryptFinal_ex(context, (uint8_t*) output + p, &added) != 1) + if (EVP_EncryptFinal_ex(context, (uint8_t*) output.iov_base + p, &added) != 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finalize data encryption: %s", ERR_error_string(ERR_get_error(), NULL)); assert(added >= 0); - assert((size_t) added <= output_size - p); + assert((size_t) added <= output.iov_len - p); p += added; - assert(p <= output_size - tsz); + assert(p <= output.iov_len - tsz); /* Append tag */ - if (EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_GET_TAG, tsz, (uint8_t*) output + p) != 1) + if (EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_GET_TAG, tsz, (uint8_t*) output.iov_base + p) != 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to get tag: %s", ERR_error_string(ERR_get_error(), NULL)); p += tsz; - assert(p <= output_size); + assert(p <= output.iov_len); + output.iov_len = p; - if (DEBUG_LOGGING && input_size > 0) { + if (DEBUG_LOGGING && input->iov_len > 0) { size_t base64_size; - base64_size = DIV_ROUND_UP(p * 4, 3); /* Include base64 size increase in debug output */ - assert(base64_size >= input_size); - log_debug("Input of %zu bytes grew to output of %zu bytes (+%2zu%%).", input_size, base64_size, base64_size * 100 / input_size - 100); + base64_size = DIV_ROUND_UP(output.iov_len * 4, 3); /* Include base64 size increase in debug output */ + assert(base64_size >= input->iov_len); + log_debug("Input of %zu bytes grew to output of %zu bytes (+%2zu%%).", input->iov_len, base64_size, base64_size * 100 / input->iov_len - 100); } - *ret = TAKE_PTR(output); - *ret_size = p; - + *ret = TAKE_STRUCT(output); return 0; } @@ -1069,39 +1178,39 @@ int decrypt_credential_and_warn( usec_t validate_timestamp, const char *tpm2_device, const char *tpm2_signature_path, - const void *input, - size_t input_size, - void **ret, - size_t *ret_size) { + uid_t uid, + const struct iovec *input, + CredentialFlags flags, + struct iovec *ret) { - _cleanup_(erase_and_freep) void *host_key = NULL, *tpm2_key = NULL, *plaintext = NULL; + _cleanup_(iovec_done_erase) struct iovec host_key = {}, plaintext = {}, tpm2_key = {}; _cleanup_(json_variant_unrefp) JsonVariant *signature_json = NULL; _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL; - size_t host_key_size = 0, tpm2_key_size = 0, plaintext_size, p, hs; struct encrypted_credential_header *h; struct metadata_credential_header *m; uint8_t md[SHA256_DIGEST_LENGTH]; - bool with_tpm2, with_host_key, is_tpm2_absent, with_tpm2_pk; + bool with_tpm2, with_tpm2_pk, with_host_key, with_null, with_scope; const EVP_CIPHER *cc; + size_t p, hs; int r, added; - assert(input || input_size == 0); + assert(iovec_is_valid(input)); assert(ret); - assert(ret_size); - h = (struct encrypted_credential_header*) input; + h = (struct encrypted_credential_header*) input->iov_base; /* The ID must fit in, for the current and all future formats */ - if (input_size < sizeof(h->id)) + 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; - is_tpm2_absent = sd_id128_equal(h->id, CRED_AES256_GCM_BY_TPM2_ABSENT); + 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 && !is_tpm2_absent) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unknown encryption format, or corrupted data: %m"); + if (!with_host_key && !with_tpm2 && !with_null) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unknown encryption format, or corrupted data."); if (with_tpm2_pk) { r = tpm2_load_pcr_signature(tpm2_signature_path, &signature_json); @@ -1109,7 +1218,7 @@ int decrypt_credential_and_warn( return log_error_errno(r, "Failed to load pcr signature: %m"); } - if (is_tpm2_absent) { + if (with_null && !FLAGS_SET(flags, CREDENTIAL_ALLOW_NULL)) { /* So this is a credential encrypted with a zero length key. We support this to cover for the * case where neither a host key not a TPM2 are available (specifically: initrd environments * where the host key is not yet accessible and no TPM2 chip exists at all), to minimize @@ -1129,8 +1238,19 @@ 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_size < offsetof(struct encrypted_credential_header, iv)) + if (input->iov_len < offsetof(struct encrypted_credential_header, iv)) return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Encrypted file too short."); /* Verify some basic header values */ @@ -1145,10 +1265,11 @@ int decrypt_credential_and_warn( /* Ensure we have space for the full header now (we don't know the size of the name hence this is a * lower limit only) */ - if (input_size < + if (input->iov_len < 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."); @@ -1157,7 +1278,7 @@ int decrypt_credential_and_warn( if (with_tpm2) { #if HAVE_TPM2 - struct tpm2_credential_header* t = (struct tpm2_credential_header*) ((uint8_t*) input + p); + struct tpm2_credential_header* t = (struct tpm2_credential_header*) ((uint8_t*) input->iov_base + p); struct tpm2_public_key_credential_header *z = NULL; if (!TPM2_PCR_MASK_VALID(t->pcr_mask)) @@ -1173,10 +1294,11 @@ int decrypt_credential_and_warn( /* Ensure we have space for the full TPM2 header now (still don't know the name, and its size * though, hence still just a lower limit test only) */ - if (input_size < - ALIGN8(offsetof(struct encrypted_credential_header, iv) + le32toh(h->iv_size)) + + if (input->iov_len < + 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."); @@ -1186,17 +1308,17 @@ int decrypt_credential_and_warn( le32toh(t->policy_hash_size)); if (with_tpm2_pk) { - z = (struct tpm2_public_key_credential_header*) ((uint8_t*) input + p); + z = (struct tpm2_public_key_credential_header*) ((uint8_t*) input->iov_base + p); if (!TPM2_PCR_MASK_VALID(le64toh(z->pcr_mask)) || le64toh(z->pcr_mask) == 0) return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "TPM2 PCR mask out of range."); if (le32toh(z->size) > PUBLIC_KEY_MAX) return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Unexpected public key size."); - if (input_size < - ALIGN8(offsetof(struct encrypted_credential_header, iv) + le32toh(h->iv_size)) + - ALIGN8(offsetof(struct tpm2_credential_header, policy_hash_and_blob) + le32toh(t->blob_size) + le32toh(t->policy_hash_size)) + + 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."); @@ -1215,21 +1337,16 @@ int decrypt_credential_and_warn( r = tpm2_unseal(tpm2_context, le64toh(t->pcr_mask), le16toh(t->pcr_bank), - z ? z->data : NULL, - z ? le32toh(z->size) : 0, + z ? &IOVEC_MAKE(z->data, le32toh(z->size)) : NULL, z ? le64toh(z->pcr_mask) : 0, signature_json, /* pin= */ NULL, /* pcrlock_policy= */ NULL, le16toh(t->primary_alg), - t->policy_hash_and_blob, - le32toh(t->blob_size), - t->policy_hash_and_blob + le32toh(t->blob_size), - le32toh(t->policy_hash_size), - /* srk_buf= */ NULL, - /* srk_buf_size= */ 0, - &tpm2_key, - &tpm2_key_size); + &IOVEC_MAKE(t->policy_hash_and_blob, le32toh(t->blob_size)), + &IOVEC_MAKE(t->policy_hash_and_blob + le32toh(t->blob_size), le32toh(t->policy_hash_size)), + /* srk= */ NULL, + &tpm2_key); if (r < 0) return log_error_errno(r, "Failed to unseal secret using TPM2: %m"); #else @@ -1237,19 +1354,38 @@ 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( - 0, - &host_key, - &host_key_size); + r = get_credential_host_secret(/* flags= */ 0, &host_key); if (r < 0) return log_error_errno(r, "Failed to determine local credential key: %m"); } - if (is_tpm2_absent) + if (with_null && !FLAGS_SET(flags, CREDENTIAL_ALLOW_NULL)) log_warning("Warning: using a null key for decryption and authentication. Confidentiality or authenticity are not provided."); - sha256_hash_host_and_tpm2_key(host_key, host_key_size, tpm2_key, tpm2_key_size, md); + 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()); @@ -1276,41 +1412,41 @@ int decrypt_credential_and_warn( return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set IV and key: %s", ERR_error_string(ERR_get_error(), NULL)); - if (EVP_DecryptUpdate(context, NULL, &added, input, p) != 1) + if (EVP_DecryptUpdate(context, NULL, &added, input->iov_base, p) != 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to write AAD data: %s", ERR_error_string(ERR_get_error(), NULL)); - plaintext = malloc(input_size - p - le32toh(h->tag_size)); - if (!plaintext) + plaintext.iov_base = malloc(input->iov_len - p - le32toh(h->tag_size)); + if (!plaintext.iov_base) return -ENOMEM; if (EVP_DecryptUpdate( context, - plaintext, + plaintext.iov_base, &added, - (uint8_t*) input + p, - input_size - p - le32toh(h->tag_size)) != 1) + (uint8_t*) input->iov_base + p, + input->iov_len - p - le32toh(h->tag_size)) != 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to decrypt data: %s", ERR_error_string(ERR_get_error(), NULL)); assert(added >= 0); - assert((size_t) added <= input_size - p - le32toh(h->tag_size)); - plaintext_size = added; + assert((size_t) added <= input->iov_len - p - le32toh(h->tag_size)); + plaintext.iov_len = added; - if (EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_SET_TAG, le32toh(h->tag_size), (uint8_t*) input + input_size - le32toh(h->tag_size)) != 1) + if (EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_SET_TAG, le32toh(h->tag_size), (uint8_t*) input->iov_base + input->iov_len - le32toh(h->tag_size)) != 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set tag: %s", ERR_error_string(ERR_get_error(), NULL)); - if (EVP_DecryptFinal_ex(context, (uint8_t*) plaintext + plaintext_size, &added) != 1) + if (EVP_DecryptFinal_ex(context, (uint8_t*) plaintext.iov_base + plaintext.iov_len, &added) != 1) return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Decryption failed (incorrect key?): %s", ERR_error_string(ERR_get_error(), NULL)); - plaintext_size += added; + plaintext.iov_len += added; - if (plaintext_size < ALIGN8(offsetof(struct metadata_credential_header, name))) + if (plaintext.iov_len < ALIGN8(offsetof(struct metadata_credential_header, name))) return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Metadata header incomplete."); - m = plaintext; + m = plaintext.iov_base; if (le64toh(m->timestamp) != USEC_INFINITY && le64toh(m->not_after) != USEC_INFINITY && @@ -1321,7 +1457,7 @@ int decrypt_credential_and_warn( return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Embedded credential name too long, refusing."); hs = ALIGN8(offsetof(struct metadata_credential_header, name) + le32toh(m->name_size)); - if (plaintext_size < hs) + if (plaintext.iov_len < hs) return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Metadata header incomplete."); if (le32toh(m->name_size) > 0) { @@ -1336,7 +1472,7 @@ int decrypt_credential_and_warn( if (validate_name && !streq(embedded_name, validate_name)) { - r = getenv_bool_secure("SYSTEMD_CREDENTIAL_VALIDATE_NAME"); + r = secure_getenv_bool("SYSTEMD_CREDENTIAL_VALIDATE_NAME"); if (r < 0 && r != -ENXIO) log_debug_errno(r, "Failed to parse $SYSTEMD_CREDENTIAL_VALIDATE_NAME: %m"); if (r != 0) @@ -1352,7 +1488,7 @@ int decrypt_credential_and_warn( if (le64toh(m->not_after) != USEC_INFINITY && le64toh(m->not_after) < validate_timestamp) { - r = getenv_bool_secure("SYSTEMD_CREDENTIAL_VALIDATE_NOT_AFTER"); + r = secure_getenv_bool("SYSTEMD_CREDENTIAL_VALIDATE_NOT_AFTER"); if (r < 0 && r != -ENXIO) log_debug_errno(r, "Failed to parse $SYSTEMD_CREDENTIAL_VALIDATE_NOT_AFTER: %m"); if (r != 0) @@ -1363,33 +1499,245 @@ int decrypt_credential_and_warn( } if (ret) { - char *without_metadata; + _cleanup_(iovec_done_erase) struct iovec without_metadata = {}; - without_metadata = memdup((uint8_t*) plaintext + hs, plaintext_size - hs); - if (!without_metadata) + without_metadata.iov_len = plaintext.iov_len - hs; + without_metadata.iov_base = memdup_suffix0((uint8_t*) plaintext.iov_base + hs, without_metadata.iov_len); + if (!without_metadata.iov_base) return log_oom(); - *ret = without_metadata; + *ret = TAKE_STRUCT(without_metadata); } - if (ret_size) - *ret_size = plaintext_size - hs; - return 0; } #else -int get_credential_host_secret(CredentialSecretFlags flags, void **ret, size_t *ret_size) { +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 void *input, size_t input_size, void **ret, size_t *ret_size) { +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 void *input, size_t input_size, void **ret, size_t *ret_size) { +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; +} + +static int pick_up_credential_one( + int credential_dir_fd, + const char *credential_name, + const PickUpCredential *table_entry) { + + _cleanup_free_ char *fn = NULL, *target_path = NULL; + const char *e; + int r; + + assert(credential_dir_fd >= 0); + assert(credential_name); + assert(table_entry); + + e = startswith(credential_name, table_entry->credential_prefix); + if (!e) + return 0; /* unmatched */ + + fn = strjoin(e, table_entry->filename_suffix); + if (!fn) + return log_oom(); + + if (!filename_is_valid(fn)) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "Passed credential '%s' would result in invalid filename '%s'.", + credential_name, fn); + + r = mkdir_p_label(table_entry->target_dir, 0755); + if (r < 0) + return log_warning_errno(r, "Failed to create '%s': %m", table_entry->target_dir); + + target_path = path_join(table_entry->target_dir, fn); + if (!target_path) + return log_oom(); + + r = copy_file_at( + credential_dir_fd, credential_name, + AT_FDCWD, target_path, + /* open_flags= */ 0, + 0644, + /* flags= */ 0); + if (r < 0) + return log_warning_errno(r, "Failed to copy credential %s → file %s: %m", + credential_name, target_path); + + log_info("Installed %s from credential.", target_path); + return 1; /* done */ +} + +int pick_up_credentials(const PickUpCredential *table, size_t n_table_entry) { + _cleanup_close_ int credential_dir_fd = -EBADF; + int r, ret = 0; + + assert(table); + assert(n_table_entry > 0); + + credential_dir_fd = open_credentials_dir(); + if (IN_SET(credential_dir_fd, -ENXIO, -ENOENT)) { + /* Credential env var not set, or dir doesn't exist. */ + log_debug("No credentials found."); + return 0; + } + if (credential_dir_fd < 0) + return log_error_errno(credential_dir_fd, "Failed to open credentials directory: %m"); + + _cleanup_free_ DirectoryEntries *des = NULL; + r = readdir_all(credential_dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des); + if (r < 0) + return log_error_errno(r, "Failed to enumerate credentials: %m"); + + FOREACH_ARRAY(i, des->entries, des->n_entries) { + struct dirent *de = *i; + + if (de->d_type != DT_REG) + continue; + + FOREACH_ARRAY(t, table, n_table_entry) { + r = pick_up_credential_one(credential_dir_fd, de->d_name, t); + if (r != 0) { + RET_GATHER(ret, r); + break; /* Done, or failed. Let's move to the next credential. */ + } + } + } + + return ret; +} |