373 lines
11 KiB
Nix
373 lines
11 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
let
|
|
cfg = config.nixosWorkstations.workstationSetup;
|
|
|
|
passwordHelper = pkgs.writeShellScript "nixos-workstations-homed-password-helper" ''
|
|
set -eu
|
|
umask 077
|
|
|
|
fail() {
|
|
printf '%s\n' "$1" >&2
|
|
exit "$2"
|
|
}
|
|
|
|
IFS= read -r current || fail INPUT_ERROR 20
|
|
IFS= read -r newpass || fail INPUT_ERROR 20
|
|
IFS= read -r confirm || fail INPUT_ERROR 20
|
|
|
|
[ "$newpass" = "$confirm" ] || fail CONFIRM_MISMATCH 21
|
|
[ -n "$current" ] && [ -n "$newpass" ] || fail INPUT_ERROR 20
|
|
|
|
runtime_dir="''${XDG_RUNTIME_DIR:-/run/user/$(${pkgs.coreutils}/bin/id -u)}"
|
|
cred_dir="$(${pkgs.coreutils}/bin/mktemp -d "$runtime_dir/nixos-workstations-passwd.XXXXXX")"
|
|
log_file="$cred_dir/homectl.log"
|
|
|
|
cleanup() {
|
|
current=""
|
|
newpass=""
|
|
confirm=""
|
|
${pkgs.coreutils}/bin/rm -f \
|
|
"$cred_dir/home.password" \
|
|
"$cred_dir/home.new-password" \
|
|
"$log_file" 2>/dev/null || true
|
|
${pkgs.coreutils}/bin/rmdir "$cred_dir" 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT HUP INT TERM
|
|
|
|
${pkgs.coreutils}/bin/printf '%s' "$current" > "$cred_dir/home.password"
|
|
${pkgs.coreutils}/bin/printf '%s' "$newpass" > "$cred_dir/home.new-password"
|
|
${pkgs.coreutils}/bin/chmod 600 "$cred_dir/home.password" "$cred_dir/home.new-password"
|
|
|
|
user="$(${pkgs.coreutils}/bin/id -un)"
|
|
|
|
current=""
|
|
newpass=""
|
|
confirm=""
|
|
|
|
set +e
|
|
CREDENTIALS_DIRECTORY="$cred_dir" \
|
|
LC_ALL=C LANG=C \
|
|
${pkgs.coreutils}/bin/timeout 60 \
|
|
${pkgs.systemd}/bin/homectl --no-ask-password --no-pager passwd "$user" \
|
|
>"$log_file" 2>&1
|
|
rc=$?
|
|
set -e
|
|
|
|
if [ "$rc" -eq 0 ]; then
|
|
printf '%s\n' OK
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$rc" -eq 124 ]; then
|
|
fail TIMEOUT_PASSWORD 124
|
|
fi
|
|
|
|
if ${pkgs.gnugrep}/bin/grep -Eqi 'password incorrect|not sufficient|bad password' "$log_file"; then
|
|
fail CURRENT_REJECTED 22
|
|
fi
|
|
|
|
if ${pkgs.gnugrep}/bin/grep -Eqi 'quality|too short|weak|dictionary' "$log_file"; then
|
|
fail NEW_REJECTED 23
|
|
fi
|
|
|
|
fail HOMECTL_PASSWD_FAILED 24
|
|
'';
|
|
|
|
pinPython = pkgs.python3.withPackages (ps: [ ps.fido2 ]);
|
|
|
|
pinHelper = pkgs.writeTextFile {
|
|
name = "nixos-workstations-pin-helper";
|
|
executable = true;
|
|
text = ''
|
|
#!${pinPython}/bin/python3
|
|
import sys
|
|
|
|
from fido2.ctap import CtapError
|
|
from fido2.ctap2 import Ctap2
|
|
from fido2.ctap2.pin import ClientPin
|
|
from fido2.hid import CAPABILITY, CtapHidDevice
|
|
|
|
def fail(token: str, code: int) -> "None":
|
|
print(token, file=sys.stderr, flush=True)
|
|
raise SystemExit(code)
|
|
|
|
def read_secret() -> str:
|
|
# Read bytes so Nix/Python escaping cannot accidentally turn the
|
|
# line terminator into part of the FIDO2 PIN. QProcess terminates
|
|
# each secret with LF; tolerate CRLF as well.
|
|
raw = sys.stdin.buffer.readline()
|
|
if raw == b"":
|
|
fail("INPUT_ERROR", 30)
|
|
if raw.endswith(bytes([10])):
|
|
raw = raw[:-1]
|
|
if raw.endswith(bytes([13])):
|
|
raw = raw[:-1]
|
|
try:
|
|
value = raw.decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
fail("INPUT_ERROR", 30)
|
|
if chr(10) in value or chr(13) in value:
|
|
fail("INPUT_ERROR", 30)
|
|
return value
|
|
|
|
new_pin = read_secret()
|
|
confirmation = read_secret()
|
|
|
|
if new_pin != confirmation:
|
|
fail("CONFIRM_MISMATCH", 31)
|
|
|
|
if len(new_pin) < 4 or len(new_pin.encode("utf-8")) > 63:
|
|
fail("PIN_POLICY", 32)
|
|
|
|
devices = []
|
|
try:
|
|
devices = list(CtapHidDevice.list_devices())
|
|
if len(devices) == 0:
|
|
fail("NO_YUBIKEY", 33)
|
|
if len(devices) > 1:
|
|
fail("MULTIPLE_YUBIKEY", 34)
|
|
|
|
device = devices[0]
|
|
if not (device.capabilities & CAPABILITY.CBOR):
|
|
fail("NO_FIDO2", 35)
|
|
|
|
ctap = Ctap2(device)
|
|
if ctap.get_info().options.get("clientPin", False):
|
|
fail("PIN_ALREADY_CONFIGURED", 36)
|
|
|
|
ClientPin(ctap).set_pin(new_pin)
|
|
|
|
if not ctap.get_info().options.get("clientPin", False):
|
|
fail("PIN_STATE_NOT_UPDATED", 37)
|
|
|
|
new_pin = ""
|
|
confirmation = ""
|
|
print("OK", flush=True)
|
|
raise SystemExit(0)
|
|
|
|
except CtapError as exc:
|
|
code = exc.code
|
|
if code == CtapError.ERR.PIN_AUTH_BLOCKED:
|
|
fail("PIN_AUTH_BLOCKED", 41)
|
|
if code == CtapError.ERR.PIN_BLOCKED:
|
|
fail("PIN_BLOCKED", 42)
|
|
if code == CtapError.ERR.PIN_POLICY_VIOLATION:
|
|
fail("PIN_POLICY", 44)
|
|
fail("PIN_FAILED", 46)
|
|
except (PermissionError, OSError):
|
|
fail("NO_YUBIKEY", 47)
|
|
except ValueError:
|
|
fail("PIN_POLICY", 48)
|
|
except SystemExit:
|
|
raise
|
|
except Exception:
|
|
fail("PIN_HELPER_ERROR", 49)
|
|
finally:
|
|
new_pin = ""
|
|
confirmation = ""
|
|
for dev in devices:
|
|
try:
|
|
dev.close()
|
|
except Exception:
|
|
pass
|
|
'';
|
|
};
|
|
|
|
fidoEnrollHelper = pkgs.writeShellScript "nixos-workstations-homed-fido-helper" ''
|
|
set -eu
|
|
umask 077
|
|
|
|
fail() {
|
|
printf '%s\n' "$1" >&2
|
|
exit "$2"
|
|
}
|
|
|
|
IFS= read -r current_password || fail INPUT_ERROR 50
|
|
IFS= read -r token_pin || fail INPUT_ERROR 50
|
|
|
|
[ -n "$current_password" ] && [ -n "$token_pin" ] || fail INPUT_ERROR 50
|
|
|
|
runtime_dir="''${XDG_RUNTIME_DIR:-/run/user/$(${pkgs.coreutils}/bin/id -u)}"
|
|
cred_dir="$(${pkgs.coreutils}/bin/mktemp -d "$runtime_dir/nixos-workstations-fido.XXXXXX")"
|
|
log_file="$cred_dir/homectl.log"
|
|
|
|
cleanup() {
|
|
current_password=""
|
|
token_pin=""
|
|
${pkgs.coreutils}/bin/rm -f \
|
|
"$cred_dir/home.password" \
|
|
"$cred_dir/home.token-pin" \
|
|
"$log_file" 2>/dev/null || true
|
|
${pkgs.coreutils}/bin/rmdir "$cred_dir" 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT HUP INT TERM
|
|
|
|
${pkgs.coreutils}/bin/printf '%s' "$current_password" > "$cred_dir/home.password"
|
|
${pkgs.coreutils}/bin/printf '%s' "$token_pin" > "$cred_dir/home.token-pin"
|
|
${pkgs.coreutils}/bin/chmod 600 "$cred_dir/home.password" "$cred_dir/home.token-pin"
|
|
|
|
user="$(${pkgs.coreutils}/bin/id -un)"
|
|
|
|
current_password=""
|
|
token_pin=""
|
|
|
|
# Stream only non-sensitive progress markers to the GUI while keeping the
|
|
# complete homectl output in the private runtime log. This lets the wizard
|
|
# tell the user exactly when a physical touch is required, including when
|
|
# systemd-homed requests presence more than once during enrollment.
|
|
set +e
|
|
set -o pipefail
|
|
CREDENTIALS_DIRECTORY="$cred_dir" \
|
|
LC_ALL=C LANG=C \
|
|
${pkgs.coreutils}/bin/timeout 120 \
|
|
${pkgs.systemd}/bin/homectl --no-ask-password --no-pager update "$user" \
|
|
--fido2-device=auto \
|
|
--fido2-with-client-pin=yes \
|
|
--fido2-with-user-presence=yes \
|
|
--fido2-with-user-verification=no \
|
|
2>&1 | while IFS= read -r line || [ -n "$line" ]; do
|
|
${pkgs.coreutils}/bin/printf '%s\n' "$line" >> "$log_file"
|
|
|
|
case "$line" in
|
|
*"Initializing FIDO2 credential on security token"*)
|
|
# The first make-credential call follows this phase and requires
|
|
# physical presence when user-presence is enabled. systemd only
|
|
# emits a generic hint here, so notify the interface immediately.
|
|
${pkgs.coreutils}/bin/printf '%s\n' 'EVENT_FIDO_INITIALIZING'
|
|
;;
|
|
*"Generating secret key on FIDO2 security token"*)
|
|
# The assertion requiring the next touch follows immediately.
|
|
${pkgs.coreutils}/bin/printf '%s\n' 'EVENT_FIDO_GENERATING'
|
|
;;
|
|
*"In order to allow secret key generation, please confirm presence on security token"*)
|
|
# Already announced by EVENT_FIDO_GENERATING: do not create a
|
|
# delayed duplicate touch request in the interface.
|
|
;;
|
|
*"Updating home directory"*)
|
|
${pkgs.coreutils}/bin/printf '%s\n' 'EVENT_FIDO_UPDATING_HOME'
|
|
;;
|
|
*"Synchronizing passwords and encryption keys"*)
|
|
${pkgs.coreutils}/bin/printf '%s\n' 'EVENT_FIDO_SYNCHRONIZING'
|
|
;;
|
|
*"confirm presence on security token"*)
|
|
# Later unlock/synchronization operations announce presence
|
|
# immediately before they wait for the physical touch.
|
|
${pkgs.coreutils}/bin/printf '%s\n' 'EVENT_FIDO_TOUCH_REQUIRED'
|
|
;;
|
|
esac
|
|
done
|
|
rc=''${PIPESTATUS[0]}
|
|
set +o pipefail
|
|
set -e
|
|
|
|
if [ "$rc" -eq 0 ]; then
|
|
printf '%s\n' OK
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$rc" -eq 124 ]; then
|
|
fail TIMEOUT_FIDO 124
|
|
fi
|
|
|
|
if ${pkgs.gnugrep}/bin/grep -Eqi 'PIN.*incorrect|Bad PIN|bad pin' "$log_file"; then
|
|
fail FIDO_BAD_PIN 51
|
|
fi
|
|
|
|
if ${pkgs.gnugrep}/bin/grep -Eqi 'password.*incorrect|not sufficient|BadPassword' "$log_file"; then
|
|
fail FIDO_BAD_PASSWORD 52
|
|
fi
|
|
|
|
if ${pkgs.gnugrep}/bin/grep -Eqi 'multiple|more than one.*FIDO|auto.*device' "$log_file"; then
|
|
fail MULTIPLE_YUBIKEY 53
|
|
fi
|
|
|
|
if ${pkgs.gnugrep}/bin/grep -Eqi 'no.*FIDO|No such device|not found|not inserted' "$log_file"; then
|
|
fail NO_YUBIKEY 54
|
|
fi
|
|
|
|
fail FIDO_ENROLL_FAILED 55
|
|
'';
|
|
|
|
workstationSetup = pkgs.stdenv.mkDerivation {
|
|
pname = "nixos-workstations-setup";
|
|
version = "1.8.6";
|
|
|
|
src = ../workstation-setup;
|
|
|
|
nativeBuildInputs = [
|
|
pkgs.cmake
|
|
pkgs.ninja
|
|
pkgs.pkg-config
|
|
pkgs.kdePackages.wrapQtAppsHook
|
|
];
|
|
|
|
buildInputs = with pkgs.kdePackages; [
|
|
qtbase
|
|
qtdeclarative
|
|
qtwayland
|
|
kirigami
|
|
];
|
|
|
|
cmakeFlags = [
|
|
"-DPASSWORD_HELPER_PATH=${passwordHelper}"
|
|
"-DPIN_HELPER_PATH=${pinHelper}"
|
|
"-DFIDO_HELPER_PATH=${fidoEnrollHelper}"
|
|
];
|
|
};
|
|
|
|
launcher = pkgs.writeShellScript "nixos-workstations-setup-launcher" ''
|
|
set -eu
|
|
|
|
current_user="$(${pkgs.coreutils}/bin/id -un)"
|
|
target_user=${lib.escapeShellArg cfg.user}
|
|
|
|
[ "$current_user" = "$target_user" ] || exit 0
|
|
|
|
state_dir="''${XDG_STATE_HOME:-$HOME/.local/state}/nixos-workstations"
|
|
password_marker="$state_dir/password-initialized"
|
|
pin_marker="$state_dir/yubikey-pin-created"
|
|
fido_marker="$state_dir/yubikey-fido-enrolled"
|
|
|
|
if [ -e "$password_marker" ] && [ -e "$pin_marker" ] && [ -e "$fido_marker" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
ulimit -c 0
|
|
${pkgs.coreutils}/bin/sleep 3
|
|
|
|
exec ${workstationSetup}/bin/nixos-workstations-setup \
|
|
--target-user "$target_user"
|
|
'';
|
|
|
|
in
|
|
{
|
|
options.nixosWorkstations.workstationSetup = {
|
|
enable = lib.mkEnableOption "assistant de première session";
|
|
|
|
user = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "alice";
|
|
description = "Utilisateur devant finaliser son authentification.";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
environment.systemPackages = [
|
|
workstationSetup
|
|
pkgs.systemd
|
|
];
|
|
|
|
environment.etc."xdg/autostart/nixos-workstations-setup.desktop".text = ''
|
|
[Desktop Entry]
|
|
Type=Application
|
|
Name=Finalisation du poste
|
|
Comment=Personnalisation sécurisée des moyens d'authentification
|
|
Exec=${launcher}
|
|
OnlyShowIn=KDE;
|
|
NoDisplay=true
|
|
StartupNotify=false
|
|
'';
|
|
};
|
|
}
|