summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdrian Vovk <adrianvovk@gmail.com>2024-01-09 00:11:43 +0100
committerLuca Boccassi <bluca@debian.org>2024-02-19 12:18:11 +0100
commit1b466c09401fe4896948b0a727ed670488a3cb07 (patch)
tree768257c65224046dfa1713b434e5c351a178376f
parentDocument blob directory behavior (diff)
downloadsystemd-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.md23
-rw-r--r--docs/USER_RECORD_BLOB_DIRS.md3
-rw-r--r--src/shared/user-record-show.c37
-rw-r--r--src/shared/user-record.c63
-rw-r--r--src/shared/user-record.h7
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_;