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
This commit is contained in:
Adrian Vovk 2024-01-08 18:11:43 -05:00 committed by Luca Boccassi
parent 4006b98da6
commit 1b466c0940
5 changed files with 128 additions and 5 deletions

View file

@ -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,

View file

@ -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).

View file

@ -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),

View file

@ -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",

View file

@ -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_;