diff options
author | Adrian Vovk <adrianvovk@gmail.com> | 2024-01-09 00:11:43 +0100 |
---|---|---|
committer | Luca Boccassi <bluca@debian.org> | 2024-02-19 12:18:11 +0100 |
commit | 1b466c09401fe4896948b0a727ed670488a3cb07 (patch) | |
tree | 768257c65224046dfa1713b434e5c351a178376f | |
parent | Document blob directory behavior (diff) | |
download | systemd-1b466c09401fe4896948b0a727ed670488a3cb07.tar.xz systemd-1b466c09401fe4896948b0a727ed670488a3cb07.zip |
user-record: Add blobDirectory and blobManifest
These fields are used to connect a JSON user record to its blob
directory, and to include the directory's contents in the record's
signature
-rw-r--r-- | docs/USER_RECORD.md | 23 | ||||
-rw-r--r-- | docs/USER_RECORD_BLOB_DIRS.md | 3 | ||||
-rw-r--r-- | src/shared/user-record-show.c | 37 | ||||
-rw-r--r-- | src/shared/user-record.c | 63 | ||||
-rw-r--r-- | src/shared/user-record.h | 7 |
5 files changed, 128 insertions, 5 deletions
diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md index 1479e53916..f28106797e 100644 --- a/docs/USER_RECORD.md +++ b/docs/USER_RECORD.md @@ -234,6 +234,16 @@ optional, when unset the user should not be considered part of any realm. A user record with a realm set is never compatible (for the purpose of updates, see above) with a user record without one set, even if the `userName` field matches. +`blobDirectory` → The absolute path to a world-readable copy of the user's blob +directory. See [Blob Directories](USER_RECORD_BLOB_DIRS.md) for more details. + +`blobManifest` → An object, which maps valid blob directory filenames (see +[Blob Directories](USER_RECORD_BLOB_DIRS.md) for requirements) to SHA256 hashes +formatted as hex strings. This exists for the purpose of including the contents +of the blob directory in the record's signature. Managers that support blob +directories and utilize signed user records (like `systemd-homed`) should use +this field to verify the contents of the blob directory whenever appropriate. + `realName` → The real name of the user, a string. This should contain the user's real ("human") name, and corresponds loosely to the GECOS field of classic UNIX user records. When converting a `struct passwd` to a JSON user @@ -758,7 +768,7 @@ These two are the only two fields specific to this section. All other fields that may be used in this section are identical to the equally named ones in the `regular` section (i.e. at the top-level object). Specifically, these are: -`iconName`, `location`, `shell`, `umask`, `environment`, `timeZone`, +`blobDirectory`, `blobManifest`, `iconName`, `location`, `shell`, `umask`, `environment`, `timeZone`, `preferredLanguage`, `additionalLanguages`, `niceLevel`, `resourceLimits`, `locked`, `notBeforeUSec`, `notAfterUSec`, `storage`, `diskSize`, `diskSizeRelative`, `skeletonDirectory`, `accessMode`, `tasksMax`, `memoryHigh`, `memoryMax`, `cpuWeight`, `ioWeight`, @@ -810,9 +820,9 @@ The following fields are defined in the `binding` section. They all have an identical format and override their equally named counterparts in the `regular` and `perMachine` sections: -`imagePath`, `homeDirectory`, `partitionUuid`, `luksUuid`, `fileSystemUuid`, -`uid`, `gid`, `storage`, `fileSystemType`, `luksCipher`, `luksCipherMode`, -`luksVolumeKeySize`. +`blobDirectory`, `imagePath`, `homeDirectory`, `partitionUuid`, `luksUuid`, +`fileSystemUuid`, `uid`, `gid`, `storage`, `fileSystemType`, `luksCipher`, +`luksCipherMode`, `luksVolumeKeySize`. ## Fields in the `status` section @@ -1102,6 +1112,7 @@ A fully featured user record associated with a home directory managed by "fileSystemUuid" : "758e88c8-5851-4a2a-b88f-e7474279c111", "gid" : 60232, "homeDirectory" : "/home/grobie", + "blobDirectory" : "/var/cache/systemd/homed/grobie/", "imagePath" : "/home/grobie.home", "luksCipher" : "aes", "luksCipherMode" : "xts-plain64", @@ -1112,6 +1123,10 @@ A fully featured user record associated with a home directory managed by "uid" : 60232 } }, + "blobManifest" : { + "avatar" : "c0636851d25a62d817ff7da4e081d1e646e42c74d0ecb53425f75fcf1ba43b52", + "login-background" : "da7ad0222a6edbc6cd095149c72d38d92fd3114f606e4b57469857ef47fade18" + }, "disposition" : "regular", "enforcePasswordPolicy" : false, "lastChangeUSec" : 1565950024279735, diff --git a/docs/USER_RECORD_BLOB_DIRS.md b/docs/USER_RECORD_BLOB_DIRS.md index d0fa759a6f..8f5dd7914b 100644 --- a/docs/USER_RECORD_BLOB_DIRS.md +++ b/docs/USER_RECORD_BLOB_DIRS.md @@ -15,7 +15,8 @@ system. The JSON User Record specifies the location of the blob directory via the `blobDirectory` field. If the field is unset, then there is no blob directory -and thus no blob files to look for. The blob directory is completely +and thus no blob files to look for. Note that `blobDirectory` can exist in the +`regular`, `perMachine`, and `status` sections. The blob directory is completely owned and managed by the service that owns the rest of the user record (as specified in the `service` field). diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c index 97235bd07b..086d344c01 100644 --- a/src/shared/user-record-show.c +++ b/src/shared/user-record-show.c @@ -3,8 +3,14 @@ #include "cap-list.h" #include "format-util.h" #include "fs-util.h" +#include "glyph-util.h" +#include "hashmap.h" +#include "hexdecoct.h" +#include "path-util.h" +#include "pretty-print.h" #include "process-util.h" #include "rlimit-util.h" +#include "sha256.h" #include "strv.h" #include "terminal-util.h" #include "user-record-show.h" @@ -213,6 +219,37 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) { printf("\n"); } + if (hr->blob_directory) { + _cleanup_free_ char **filenames = NULL; + size_t n_filenames = 0; + + r = hashmap_dump_keys_sorted(hr->blob_manifest, (void***) &filenames, &n_filenames); + if (r < 0) { + errno = -r; + printf(" Blob Dir.: %s (can't iterate: %m)\n", hr->blob_directory); + } else + printf(" Blob Dir.: %s\n", hr->blob_directory); + + for (size_t i = 0; i < n_filenames; i++) { + _cleanup_free_ char *path = NULL, *link = NULL, *hash = NULL; + const char *filename = filenames[i]; + const uint8_t *hash_bytes = hashmap_get(hr->blob_manifest, filename); + bool last = i == n_filenames - 1; + + path = path_join(hr->blob_directory, filename); + if (path) + (void) terminal_urlify_path(path, filename, &link); + hash = hexmem(hash_bytes, SHA256_DIGEST_SIZE); + + printf(" %s %s %s(%s)%s\n", + special_glyph(last ? SPECIAL_GLYPH_TREE_RIGHT : SPECIAL_GLYPH_TREE_BRANCH), + link ?: filename, + ansi_grey(), + hash ?: "can't display hash", + ansi_normal()); + } + } + storage = user_record_storage(hr); if (storage >= 0) /* Let's be political, and clarify which storage we like, and which we don't. About CIFS we don't complain. */ printf(" Storage: %s%s\n", user_storage_to_string(storage), diff --git a/src/shared/user-record.c b/src/shared/user-record.c index 38e5f01c23..fc39194ac5 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -15,11 +15,13 @@ #include "path-util.h" #include "pkcs11-util.h" #include "rlimit-util.h" +#include "sha256.h" #include "string-table.h" #include "strv.h" #include "uid-classification.h" #include "user-record.h" #include "user-util.h" +#include "utf8.h" #define DEFAULT_RATELIMIT_BURST 30 #define DEFAULT_RATELIMIT_INTERVAL_USEC (1*USEC_PER_MINUTE) @@ -142,6 +144,9 @@ static UserRecord* user_record_free(UserRecord *h) { free(h->location); free(h->icon_name); + free(h->blob_directory); + hashmap_free(h->blob_manifest); + free(h->shell); strv_free(h->environment); @@ -1074,6 +1079,7 @@ static int dispatch_privileged(const char *name, JsonVariant *variant, JsonDispa static int dispatch_binding(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { static const JsonDispatch binding_dispatch_table[] = { + { "blobDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, blob_directory), 0 }, { "imagePath", JSON_VARIANT_STRING, json_dispatch_image_path, offsetof(UserRecord, image_path), 0 }, { "homeDirectory", JSON_VARIANT_STRING, json_dispatch_home_directory, offsetof(UserRecord, home_directory), 0 }, { "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 }, @@ -1110,6 +1116,52 @@ static int dispatch_binding(const char *name, JsonVariant *variant, JsonDispatch return json_dispatch(m, binding_dispatch_table, flags, userdata); } +static int dispatch_blob_manifest(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + _cleanup_hashmap_free_ Hashmap *manifest = NULL; + Hashmap **ret = ASSERT_PTR(userdata); + JsonVariant *value; + const char *key; + int r; + + if (!variant) + return 0; + + if (!json_variant_is_object(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name)); + + JSON_VARIANT_OBJECT_FOREACH(key, value, variant) { + _cleanup_free_ char *filename = NULL; + _cleanup_free_ uint8_t *hash = NULL; + + if (!json_variant_is_string(value)) + return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Blob entry '%s' has invalid hash.", key); + + if (!suitable_blob_filename(key)) + return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Blob entry '%s' has invalid filename.", key); + + filename = strdup(key); + if (!filename) + return json_log_oom(value, flags); + + hash = malloc(SHA256_DIGEST_SIZE); + if (!hash) + return json_log_oom(value, flags); + + r = parse_sha256(json_variant_string(value), hash); + if (r < 0) + return json_log(value, flags, r, "Blob entry '%s' has invalid hash: %s", filename, json_variant_string(value)); + + r = hashmap_ensure_put(&manifest, &path_hash_ops_free_free, filename, hash); + if (r < 0) + return json_log(value, flags, r, "Failed to insert blob manifest entry '%s': %m", filename); + TAKE_PTR(filename); /* Ownership transfers to hashmap */ + TAKE_PTR(hash); + } + + hashmap_free_and_replace(*ret, manifest); + return 0; +} + int per_machine_id_match(JsonVariant *ids, JsonDispatchFlags flags) { sd_id128_t mid; int r; @@ -1226,6 +1278,8 @@ static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDisp static const JsonDispatch per_machine_dispatch_table[] = { { "matchMachineId", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, { "matchHostname", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "blobDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, blob_directory), 0 }, + { "blobManifest", JSON_VARIANT_OBJECT, dispatch_blob_manifest, offsetof(UserRecord, blob_manifest), 0 }, { "iconName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, icon_name), JSON_SAFE }, { "location", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, location), 0 }, { "shell", JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, shell), 0 }, @@ -1560,6 +1614,8 @@ int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags load_fla static const JsonDispatch user_dispatch_table[] = { { "userName", JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(UserRecord, user_name), JSON_RELAX}, { "realm", JSON_VARIANT_STRING, json_dispatch_realm, offsetof(UserRecord, realm), 0 }, + { "blobDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, blob_directory), 0 }, + { "blobManifest", JSON_VARIANT_OBJECT, dispatch_blob_manifest, offsetof(UserRecord, blob_manifest), 0 }, { "realName", JSON_VARIANT_STRING, json_dispatch_gecos, offsetof(UserRecord, real_name), 0 }, { "emailAddress", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, email_address), JSON_SAFE }, { "iconName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, icon_name), JSON_SAFE }, @@ -2373,6 +2429,13 @@ int user_record_test_password_change_required(UserRecord *h) { return change_permitted ? 0 : -EROFS; } +int suitable_blob_filename(const char *name) { + /* Enforces filename requirements as described in docs/USER_RECORD_BULK_DIRS.md */ + return filename_is_valid(name) && + in_charset(name, URI_UNRESERVED) && + name[0] != '.'; +} + static const char* const user_storage_table[_USER_STORAGE_MAX] = { [USER_CLASSIC] = "classic", [USER_LUKS] = "luks", diff --git a/src/shared/user-record.h b/src/shared/user-record.h index ee63a5364c..1819f55489 100644 --- a/src/shared/user-record.h +++ b/src/shared/user-record.h @@ -6,6 +6,7 @@ #include "sd-id128.h" +#include "hashmap.h" #include "json.h" #include "missing_resource.h" #include "time-util.h" @@ -243,6 +244,9 @@ typedef struct UserRecord { char *icon_name; char *location; + char *blob_directory; + Hashmap *blob_manifest; + UserDisposition disposition; uint64_t last_change_usec; uint64_t last_password_change_usec; @@ -449,6 +453,9 @@ int per_machine_hostname_match(JsonVariant *hns, JsonDispatchFlags flags); int per_machine_match(JsonVariant *entry, JsonDispatchFlags flags); int user_group_record_mangle(JsonVariant *v, UserRecordLoadFlags load_flags, JsonVariant **ret_variant, UserRecordMask *ret_mask); +#define BLOB_DIR_MAX_SIZE (UINT64_C(64) * U64_MB) +int suitable_blob_filename(const char *name); + const char* user_storage_to_string(UserStorage t) _const_; UserStorage user_storage_from_string(const char *s) _pure_; |