homectl: write recovery key file via linkable tmpfile
Create the file with open_tmpfile_linkable_at(). Write the recovery key and trailing newline to the temporary fd. Set the final mode to 0600 before publishing it. Link it into place with link_tmpfile_at(..., LINK_TMPFILE_SYNC).
This commit is contained in:
committed by
Yu Watanabe
parent
3f72568682
commit
471e34d5cb
@@ -692,6 +692,17 @@
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v247"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><option>--recovery-key-file=<replaceable>PATH</replaceable></option></term>
|
||||
|
||||
<listitem><para>Attempts to write the generated recovery key to the specified file. The file is
|
||||
created with access mode <literal>0600</literal> and must not exist yet. This option may only be used
|
||||
together with <option>--recovery-key=yes</option>. If the file cannot be written, a warning is shown
|
||||
and the account operation still succeeds. The recovery key is always shown on screen.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
</refsect1>
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ _homectl() {
|
||||
--language
|
||||
--ssh-authorized-keys
|
||||
--pkcs11-token-uri
|
||||
--recovery-key
|
||||
--recovery-key-file
|
||||
--locked
|
||||
--not-before
|
||||
--not-after
|
||||
@@ -126,7 +128,7 @@ _homectl() {
|
||||
--machine|-M)
|
||||
comps=$( __get_machines )
|
||||
;;
|
||||
--identity|--image-path|--avatar|--login-background)
|
||||
--identity|--image-path|--avatar|--login-background|--recovery-key-file)
|
||||
comps=$(compgen -A file -- "$cur" )
|
||||
compopt -o filenames
|
||||
;;
|
||||
@@ -136,7 +138,7 @@ _homectl() {
|
||||
--export-format)
|
||||
comps='full stripped minimal'
|
||||
;;
|
||||
--locked|--enforce-password-policy|--password-change-now|--luks-discard|--luks-offline-discard|--nosuid|--nodev|--noexec|--kill-processes|--auto-login)
|
||||
--locked|--recovery-key|--enforce-password-policy|--password-change-now|--luks-discard|--luks-offline-discard|--nosuid|--nodev|--noexec|--kill-processes|--auto-login)
|
||||
comps='yes no'
|
||||
;;
|
||||
-d|--home-dir|--skel|-b|--blob)
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "ansi-color.h"
|
||||
#include "chase.h"
|
||||
#include "errno-util.h"
|
||||
#include "fd-util.h"
|
||||
#include "glyph-util.h"
|
||||
#include "homectl-recovery-key.h"
|
||||
#include "io-util.h"
|
||||
#include "json-util.h"
|
||||
#include "libcrypt-util.h"
|
||||
#include "log.h"
|
||||
#include "memory-util.h"
|
||||
#include "path-util.h"
|
||||
#include "qrcode-util.h"
|
||||
#include "recovery-key.h"
|
||||
#include "strv.h"
|
||||
#include "tmpfile-util.h"
|
||||
|
||||
static int add_privileged(sd_json_variant **v, const char *hashed) {
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *e = NULL, *w = NULL, *l = NULL;
|
||||
@@ -103,11 +113,12 @@ static int add_secret(sd_json_variant **v, const char *password) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int identity_add_recovery_key(sd_json_variant **v) {
|
||||
int identity_add_recovery_key(sd_json_variant **v, char **ret_recovery_key) {
|
||||
_cleanup_(erase_and_freep) char *password = NULL, *hashed = NULL;
|
||||
int r;
|
||||
|
||||
assert(v);
|
||||
assert(ret_recovery_key);
|
||||
|
||||
/* First, let's generate a secret key */
|
||||
r = make_recovery_key(&password);
|
||||
@@ -134,6 +145,13 @@ int identity_add_recovery_key(sd_json_variant **v) {
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
*ret_recovery_key = TAKE_PTR(password);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void show_recovery_key(const char *recovery_key) {
|
||||
assert(recovery_key);
|
||||
|
||||
/* We output the key itself with a trailing newline to stdout and the decoration around it to stderr
|
||||
* instead. */
|
||||
|
||||
@@ -146,7 +164,7 @@ int identity_add_recovery_key(sd_json_variant **v) {
|
||||
ansi_highlight());
|
||||
fflush(stderr);
|
||||
|
||||
fputs(password, stdout);
|
||||
fputs(recovery_key, stdout);
|
||||
fflush(stdout);
|
||||
|
||||
fputs(ansi_normal(), stderr);
|
||||
@@ -161,7 +179,48 @@ int identity_add_recovery_key(sd_json_variant **v) {
|
||||
"whenever authentication is requested.\n", stderr);
|
||||
fflush(stderr);
|
||||
|
||||
(void) print_qrcode(stderr, "Optionally scan the recovery key for safekeeping", password);
|
||||
(void) print_qrcode(stderr, "Optionally scan the recovery key for safekeeping", recovery_key);
|
||||
}
|
||||
|
||||
int recovery_key_file_write(const char *path, const char *recovery_key) {
|
||||
_cleanup_free_ char *filename = NULL, *tmp = NULL;
|
||||
_cleanup_close_ int dir_fd = -EBADF, fd = -EBADF;
|
||||
int r;
|
||||
|
||||
assert(path);
|
||||
assert(recovery_key);
|
||||
|
||||
r = chase(path,
|
||||
/* root= */ NULL,
|
||||
CHASE_PARENT|CHASE_EXTRACT_FILENAME|CHASE_PROHIBIT_SYMLINKS,
|
||||
&filename,
|
||||
&dir_fd);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (!filename_is_valid(filename))
|
||||
return -EINVAL;
|
||||
|
||||
fd = open_tmpfile_linkable_at(dir_fd, filename, O_WRONLY|O_CLOEXEC, &tmp);
|
||||
if (fd < 0)
|
||||
return fd;
|
||||
|
||||
CLEANUP_TMPFILE_AT(dir_fd, tmp);
|
||||
|
||||
if (fchmod(fd, 0600) < 0)
|
||||
return -errno;
|
||||
|
||||
r = loop_write(fd, recovery_key, strlen(recovery_key));
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = loop_write(fd, "\n", 1);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = link_tmpfile_at(fd, dir_fd, tmp, filename, LINK_TMPFILE_SYNC);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
tmp = mfree(tmp); /* disarm CLEANUP_TMPFILE_AT() */
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -3,4 +3,6 @@
|
||||
|
||||
#include "forward.h"
|
||||
|
||||
int identity_add_recovery_key(sd_json_variant **v);
|
||||
int identity_add_recovery_key(sd_json_variant **v, char **ret_recovery_key);
|
||||
void show_recovery_key(const char *recovery_key);
|
||||
int recovery_key_file_write(const char *path, const char *recovery_key);
|
||||
|
||||
+49
-6
@@ -106,6 +106,7 @@ static int arg_fido2_cred_alg = COSE_ES256;
|
||||
static int arg_fido2_cred_alg = 0;
|
||||
#endif
|
||||
static bool arg_recovery_key = false;
|
||||
static char *arg_recovery_key_file = NULL;
|
||||
static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
|
||||
static ExportFormat arg_export_format = EXPORT_FORMAT_FULL;
|
||||
static uint64_t arg_capability_bounding_set = CAP_MASK_UNSET;
|
||||
@@ -131,6 +132,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_identity_filter, strv_freep);
|
||||
STATIC_DESTRUCTOR_REGISTER(arg_identity_filter_rlimits, strv_freep);
|
||||
STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, strv_freep);
|
||||
STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, strv_freep);
|
||||
STATIC_DESTRUCTOR_REGISTER(arg_recovery_key_file, freep);
|
||||
STATIC_DESTRUCTOR_REGISTER(arg_blob_dir, freep);
|
||||
STATIC_DESTRUCTOR_REGISTER(arg_blob_files, hashmap_freep);
|
||||
STATIC_DESTRUCTOR_REGISTER(arg_key_name, freep);
|
||||
@@ -157,6 +159,7 @@ static bool identity_properties_specified(void) {
|
||||
!strv_isempty(arg_identity_filter_rlimits) ||
|
||||
!strv_isempty(arg_pkcs11_token_uri) ||
|
||||
!strv_isempty(arg_fido2_device) ||
|
||||
arg_recovery_key ||
|
||||
arg_blob_dir ||
|
||||
arg_blob_clear ||
|
||||
!hashmap_isempty(arg_blob_files);
|
||||
@@ -1016,12 +1019,14 @@ static int add_disposition(sd_json_variant **v) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int acquire_new_home_record(sd_json_variant *input, UserRecord **ret) {
|
||||
static int acquire_new_home_record(sd_json_variant *input, UserRecord **ret, char **ret_recovery_key) {
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
|
||||
_cleanup_(erase_and_freep) char *recovery_key = NULL;
|
||||
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
|
||||
int r;
|
||||
|
||||
assert(ret);
|
||||
assert(ret_recovery_key);
|
||||
|
||||
if (arg_identity) {
|
||||
unsigned line = 0, column = 0;
|
||||
@@ -1062,7 +1067,7 @@ static int acquire_new_home_record(sd_json_variant *input, UserRecord **ret) {
|
||||
}
|
||||
|
||||
if (arg_recovery_key) {
|
||||
r = identity_add_recovery_key(&v);
|
||||
r = identity_add_recovery_key(&v, &recovery_key);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
@@ -1094,6 +1099,7 @@ static int acquire_new_home_record(sd_json_variant *input, UserRecord **ret) {
|
||||
return r;
|
||||
|
||||
*ret = TAKE_PTR(hr);
|
||||
*ret_recovery_key = TAKE_PTR(recovery_key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1316,13 +1322,31 @@ static int bus_message_append_blobs(sd_bus_message *m, Hashmap *blobs) {
|
||||
return sd_bus_message_close_container(m);
|
||||
}
|
||||
|
||||
static void finalize_recovery_key(const char *recovery_key, const char *recovery_key_file) {
|
||||
int r;
|
||||
|
||||
if (!recovery_key)
|
||||
return;
|
||||
|
||||
if (recovery_key_file) {
|
||||
r = recovery_key_file_write(recovery_key_file, recovery_key);
|
||||
if (r < 0)
|
||||
log_warning_errno(r,
|
||||
"Failed to write recovery key file '%s', ignoring: %m",
|
||||
recovery_key_file);
|
||||
}
|
||||
|
||||
show_recovery_key(recovery_key);
|
||||
}
|
||||
|
||||
static int create_home_common(sd_json_variant *input, bool show_enforce_password_policy_hint) {
|
||||
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
|
||||
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
|
||||
_cleanup_hashmap_free_ Hashmap *blobs = NULL;
|
||||
_cleanup_(erase_and_freep) char *recovery_key = NULL;
|
||||
int r;
|
||||
|
||||
r = acquire_new_home_record(input, &hr);
|
||||
r = acquire_new_home_record(input, &hr, &recovery_key);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
@@ -1369,6 +1393,7 @@ static int create_home_common(sd_json_variant *input, bool show_enforce_password
|
||||
|
||||
if (arg_dry_run) {
|
||||
sd_json_variant_dump(hr->json, SD_JSON_FORMAT_COLOR_AUTO|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_NEWLINE, stderr, /* prefix= */ NULL);
|
||||
finalize_recovery_key(recovery_key, /* recovery_key_file= */ NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1430,6 +1455,7 @@ static int create_home_common(sd_json_variant *input, bool show_enforce_password
|
||||
break; /* done */
|
||||
}
|
||||
|
||||
finalize_recovery_key(recovery_key, arg_recovery_key_file);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1473,13 +1499,16 @@ static int verb_create_home(int argc, char *argv[], uintptr_t _data, void *userd
|
||||
static int acquire_updated_home_record(
|
||||
sd_bus *bus,
|
||||
const char *username,
|
||||
UserRecord **ret) {
|
||||
UserRecord **ret,
|
||||
char **ret_recovery_key) {
|
||||
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
|
||||
_cleanup_(erase_and_freep) char *recovery_key = NULL;
|
||||
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
|
||||
int r;
|
||||
|
||||
assert(ret);
|
||||
assert(ret_recovery_key);
|
||||
|
||||
if (arg_identity) {
|
||||
unsigned line = 0, column = 0;
|
||||
@@ -1561,7 +1590,7 @@ static int acquire_updated_home_record(
|
||||
}
|
||||
|
||||
if (arg_recovery_key) {
|
||||
r = identity_add_recovery_key(&json);
|
||||
r = identity_add_recovery_key(&json, &recovery_key);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
@@ -1584,6 +1613,7 @@ static int acquire_updated_home_record(
|
||||
return r;
|
||||
|
||||
*ret = TAKE_PTR(hr);
|
||||
*ret_recovery_key = TAKE_PTR(recovery_key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1617,6 +1647,7 @@ static int verb_update_home(int argc, char *argv[], uintptr_t _data, void *userd
|
||||
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
|
||||
_cleanup_(user_record_unrefp) UserRecord *hr = NULL, *secret = NULL;
|
||||
_cleanup_free_ char *buffer = NULL;
|
||||
_cleanup_(erase_and_freep) char *recovery_key = NULL;
|
||||
_cleanup_hashmap_free_ Hashmap *blobs = NULL;
|
||||
const char *username;
|
||||
uint64_t flags = 0;
|
||||
@@ -1642,7 +1673,7 @@ static int verb_update_home(int argc, char *argv[], uintptr_t _data, void *userd
|
||||
|
||||
(void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
|
||||
|
||||
r = acquire_updated_home_record(bus, username, &hr);
|
||||
r = acquire_updated_home_record(bus, username, &hr, &recovery_key);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
@@ -1661,6 +1692,7 @@ static int verb_update_home(int argc, char *argv[], uintptr_t _data, void *userd
|
||||
|
||||
if (arg_dry_run) {
|
||||
sd_json_variant_dump(hr->json, SD_JSON_FORMAT_COLOR_AUTO|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_NEWLINE, stderr, /* prefix= */ NULL);
|
||||
finalize_recovery_key(recovery_key, /* recovery_key_file= */ NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1716,6 +1748,8 @@ static int verb_update_home(int argc, char *argv[], uintptr_t _data, void *userd
|
||||
break;
|
||||
}
|
||||
|
||||
finalize_recovery_key(recovery_key, arg_recovery_key_file);
|
||||
|
||||
if (and_resize)
|
||||
log_info("Resizing home.");
|
||||
|
||||
@@ -4370,6 +4404,12 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) {
|
||||
return r;
|
||||
break;
|
||||
|
||||
OPTION_LONG("recovery-key-file", "PATH", "Write the generated recovery key to a file"):
|
||||
r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_recovery_key_file);
|
||||
if (r < 0)
|
||||
return r;
|
||||
break;
|
||||
|
||||
OPTION_GROUP("Blob Directory User Record Properties"): {}
|
||||
|
||||
OPTION('b', "blob", "[FILENAME=]PATH",
|
||||
@@ -4877,6 +4917,9 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) {
|
||||
}
|
||||
}
|
||||
|
||||
if (arg_recovery_key_file && !arg_recovery_key)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--recovery-key-file= requires --recovery-key=yes.");
|
||||
|
||||
*remaining_args = option_parser_get_args(&opts);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -267,6 +267,76 @@ testcase_basic() {
|
||||
homectl remove test-user
|
||||
}
|
||||
|
||||
testcase_recovery_key_file() (
|
||||
local KEY_DIR
|
||||
KEY_DIR="$(mktemp -d /tmp/homed-recovery-key-file.XXXXXX)"
|
||||
local KEY_FILE="$KEY_DIR/recovery-key"
|
||||
local KEY_FILE2="$KEY_DIR/recovery-key-update"
|
||||
local DRY_RUN_OUTPUT="$KEY_DIR/dry-run.out"
|
||||
local DRY_RUN_ERROR="$KEY_DIR/dry-run.err"
|
||||
local UPDATE_EXISTING_OUTPUT="$KEY_DIR/update-existing.out"
|
||||
local KEY_FILE_CONTENT
|
||||
|
||||
homectl remove recoverykeyfiletest 2>/dev/null || true
|
||||
|
||||
trap 'homectl remove recoverykeyfiletest 2>/dev/null || true; rm -rf "$KEY_DIR"' EXIT ERR
|
||||
|
||||
(! NEWPASSWORD=Secr3tRecovery \
|
||||
homectl create recoverykeyfiletest \
|
||||
--storage=directory \
|
||||
--recovery-key-file="$KEY_FILE" \
|
||||
--enforce-password-policy=no )
|
||||
|
||||
SYSTEMD_HOME_DRY_RUN=1 \
|
||||
NEWPASSWORD=Secr3tRecovery \
|
||||
homectl create recoverykeyfiletest \
|
||||
--storage=directory \
|
||||
--recovery-key=yes \
|
||||
--recovery-key-file="$KEY_FILE" \
|
||||
--enforce-password-policy=no \
|
||||
>"$DRY_RUN_OUTPUT" \
|
||||
2>"$DRY_RUN_ERROR"
|
||||
grep -E '^[cbdefghijklnrtuv]{8}(-[cbdefghijklnrtuv]{8}){7}$' "$DRY_RUN_OUTPUT" >/dev/null
|
||||
test ! -e "$KEY_FILE"
|
||||
|
||||
NEWPASSWORD=Secr3tRecovery \
|
||||
homectl create recoverykeyfiletest \
|
||||
--storage=directory \
|
||||
--recovery-key=yes \
|
||||
--recovery-key-file="$KEY_FILE" \
|
||||
--rate-limit-interval=1s \
|
||||
--rate-limit-burst=1000 \
|
||||
--enforce-password-policy=no
|
||||
|
||||
test -s "$KEY_FILE"
|
||||
[[ "$(stat -c "%a" "$KEY_FILE")" == "600" ]]
|
||||
grep -E '^[cbdefghijklnrtuv]{8}(-[cbdefghijklnrtuv]{8}){7}$' "$KEY_FILE" >/dev/null
|
||||
PASSWORD="$(cat "$KEY_FILE")" homectl authenticate recoverykeyfiletest
|
||||
|
||||
KEY_FILE_CONTENT="$(cat "$KEY_FILE")"
|
||||
PASSWORD=Secr3tRecovery \
|
||||
homectl update recoverykeyfiletest \
|
||||
--recovery-key=yes \
|
||||
--recovery-key-file="$KEY_FILE" \
|
||||
>"$UPDATE_EXISTING_OUTPUT"
|
||||
[[ "$(cat "$KEY_FILE")" == "$KEY_FILE_CONTENT" ]]
|
||||
grep -E '^[cbdefghijklnrtuv]{8}(-[cbdefghijklnrtuv]{8}){7}$' "$UPDATE_EXISTING_OUTPUT" >/dev/null
|
||||
PASSWORD="$(cat "$UPDATE_EXISTING_OUTPUT")" homectl authenticate recoverykeyfiletest
|
||||
|
||||
PASSWORD=Secr3tRecovery \
|
||||
homectl update recoverykeyfiletest \
|
||||
--recovery-key=yes \
|
||||
--recovery-key-file="$KEY_FILE2"
|
||||
|
||||
test -s "$KEY_FILE2"
|
||||
[[ "$(stat -c "%a" "$KEY_FILE2")" == "600" ]]
|
||||
grep -E '^[cbdefghijklnrtuv]{8}(-[cbdefghijklnrtuv]{8}){7}$' "$KEY_FILE2" >/dev/null
|
||||
PASSWORD="$(cat "$KEY_FILE2")" homectl authenticate recoverykeyfiletest
|
||||
|
||||
homectl remove recoverykeyfiletest
|
||||
rm -rf "$KEY_DIR"
|
||||
)
|
||||
|
||||
testcase_blob() {
|
||||
# blob directory tests
|
||||
# See docs/USER_RECORD_BLOB_DIRS.md
|
||||
@@ -1115,6 +1185,7 @@ testcase_identity_groups() {
|
||||
|
||||
machinectl shell idgrouptest@ /usr/bin/bash -euxo pipefail -c "jq '.memberOf = ((.memberOf // []) + [\"systemd-journal\"] | unique) | .lastChangeUSec = ((.lastChangeUSec // 0) + 3600000000)' /home/idgrouptest/.identity > /home/idgrouptest/.identity.new && mv -f /home/idgrouptest/.identity.new /home/idgrouptest/.identity"
|
||||
jq -e '.memberOf | index("systemd-journal") != null' /home/idgrouptest/.identity
|
||||
wait_for_state idgrouptest active
|
||||
|
||||
PASSWORD=foobar homectl authenticate idgrouptest
|
||||
|
||||
|
||||
Reference in New Issue
Block a user