From 2ab3c19731e3625729b5b5259c9745c394883d64 Mon Sep 17 00:00:00 2001 From: Olivier <1+olivier@noreply.raspot.in> Date: Wed, 19 Aug 2026 11:40:56 +0200 Subject: [PATCH] =?UTF-8?q?T=C3=A9l=C3=A9verser=20les=20fichiers=20vers=20?= =?UTF-8?q?"modules"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/security.nix | 47 ++++--------------------- modules/software.nix | 1 - modules/workstation-setup.nix | 65 ++++++++++++++++------------------- 3 files changed, 36 insertions(+), 77 deletions(-) diff --git a/modules/security.nix b/modules/security.nix index 15efa31..d463f5a 100644 --- a/modules/security.nix +++ b/modules/security.nix @@ -1,44 +1,11 @@ { ... }: { - # Module NixOS officiel : installe YubiKey Manager et les règles udev - # nécessaires pour que ykman puisse accéder à la clé en session utilisateur. + # Étape 1 LAB : la YubiKey est uniquement initialisée par le setup. + # Aucune authentification PAM/FIDO2 n'est activée à ce stade et aucun + # credential spécifique à une clé n'est conservé dans Git. + # + # YubiKey Manager apporte notamment les règles udev permettant l'accès + # utilisateur à la clé. L'enrôlement systemd-homed viendra à l'étape 3. programs.yubikey-manager.enable = true; - - # Déploiement du mapping utilisateur <-> YubiKey - environment.etc."u2f-mappings".source = ../u2f-mappings; - - # Authentification PAM avec FIDO2 / YubiKey - security.pam.u2f = { - enable = true; - - # Une authentification YubiKey réussie suffit. - # Le mot de passe reste disponible comme mécanisme de secours - # pour les comptes qui ne s'authentifient pas par FIDO2. - control = "sufficient"; - - settings = { - authfile = "/etc/u2f-mappings"; - - # Identifiant commun à tous nos futurs postes. - origin = "pam://nixos-workstations"; - appid = "pam://nixos-workstations"; - - # Indique à l'utilisateur qu'il doit toucher la clé. - cue = true; - - # Le PIN FIDO2 est maintenant obligatoire. - pinverification = 1; - - # Pas de biométrie / vérification utilisateur intégrée. - # La vérification repose explicitement sur le PIN. - userverification = 0; - }; - }; - # Le changement de mot de passe via `passwd` doit rester une opération - # PAM/Unix classique. Le module U2F global ne doit pas intercepter - # l'authentification demandée par `passwd`, sinon le helper attend une - # interaction FIDO2 qu'il ne doit pas gérer à cette étape. - security.pam.services.passwd.u2f.enable = false; - -} \ No newline at end of file +} diff --git a/modules/software.nix b/modules/software.nix index fa97f38..b0c548a 100644 --- a/modules/software.nix +++ b/modules/software.nix @@ -35,7 +35,6 @@ in outlookWeb # FIDO2 / YubiKey - pam_u2f libfido2 yubikey-manager ]; diff --git a/modules/workstation-setup.nix b/modules/workstation-setup.nix index 69d2331..097926e 100644 --- a/modules/workstation-setup.nix +++ b/modules/workstation-setup.nix @@ -136,15 +136,20 @@ let executable = true; text = '' #!${pinPython}/bin/python3 - """Change a FIDO2 PIN without exposing secrets in argv. + """Initialize the PIN of a virgin FIDO2 authenticator. + + STEP 1 LAB contract: + - exactly one FIDO2 HID device must be connected; + - the authenticator must support CTAP2; + - no FIDO2 PIN must currently be configured; + - the new PIN and its confirmation arrive on stdin; + - secrets are never placed in argv or emitted in output. Input on stdin, one UTF-8 line each: - 1. current PIN - 2. new PIN - 3. confirmation + 1. new PIN + 2. confirmation - Output contains technical tokens only, never the PIN values. - Exactly one CTAP2 change_pin operation is attempted. + Output contains technical tokens only, never PIN values. """ import sys @@ -167,22 +172,16 @@ let return value.rstrip("\r\n") - current = read_secret() new_pin = read_secret() confirmation = read_secret() if new_pin != confirmation: fail("CONFIRM_MISMATCH", 31) - if len(current) < 4 or len(new_pin) < 4: - fail("PIN_POLICY", 32) - - # CTAP2 PINs are UTF-8 strings with a maximum encoded size of 63 bytes. - if len(current.encode("utf-8")) > 63 or len(new_pin.encode("utf-8")) > 63: + if len(new_pin) < 4 or len(new_pin.encode("utf-8")) > 63: fail("PIN_POLICY", 32) devices = [] - device = None try: devices = list(CtapHidDevice.list_devices()) @@ -201,57 +200,51 @@ let ctap = Ctap2(device) info = ctap.get_info() - # clientPin=True means a PIN is currently configured. - if not info.options.get("clientPin", False): - fail("PIN_NOT_CONFIGURED", 36) + # Étape 1 stricte : on ne doit jamais tenter de valider/changer + # un PIN existant. Ce contrôle ne consomme aucune tentative de PIN. + if info.options.get("clientPin", False): + fail("PIN_ALREADY_CONFIGURED", 36) client_pin = ClientPin(ctap) + client_pin.set_pin(new_pin) - # IMPORTANT: exactly one PIN-changing command. Do not verify the old PIN - # first: that would add an unnecessary extra PIN operation. - client_pin.change_pin(current, new_pin) + # GET_INFO ne consomme pas de tentative de PIN. Vérifier que la clé + # annonce désormais bien clientPin=True avant de déclarer le succès. + if not ctap.get_info().options.get("clientPin", False): + fail("PIN_STATE_NOT_UPDATED", 37) - # Forget Python references as soon as the operation is complete. - current = "" new_pin = "" confirmation = "" - print("OK", flush=True) raise SystemExit(0) except CtapError as exc: code = exc.code - if code == CtapError.ERR.PIN_INVALID: - fail("WRONG_PIN", 40) 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_NOT_SET: - fail("PIN_NOT_CONFIGURED", 43) if code == CtapError.ERR.PIN_POLICY_VIOLATION: fail("PIN_POLICY", 44) + if code == CtapError.ERR.PIN_NOT_SET: + fail("PIN_STATE_ERROR", 45) - fail("PIN_FAILED", 45) + fail("PIN_FAILED", 46) except (PermissionError, OSError): - fail("NO_YUBIKEY", 46) + fail("NO_YUBIKEY", 47) except ValueError: - # python-fido2 may reject an invalid PIN before sending CTAP2. - fail("PIN_POLICY", 47) + fail("PIN_POLICY", 48) except SystemExit: raise except Exception: - # Do not expose exception text: it could contain device/environment - # details and is not needed by the UI. The setup log stores the token. - fail("PIN_HELPER_ERROR", 48) + fail("PIN_HELPER_ERROR", 49) finally: - current = "" new_pin = "" confirmation = "" for dev in devices: @@ -264,7 +257,7 @@ let workstationSetup = pkgs.stdenv.mkDerivation { pname = "nixos-workstations-setup"; - version = "1.5.0"; + version = "1.6.0"; src = ../workstation-setup; @@ -301,7 +294,7 @@ let state_dir="''${XDG_STATE_HOME:-$HOME/.local/state}/nixos-workstations" password_marker="$state_dir/password-initialized" - pin_marker="$state_dir/yubikey-pin-initialized" + pin_marker="$state_dir/yubikey-pin-created" # Une session déjà finalisée n'affiche plus l'assistant. if [ -e "$password_marker" ] && [ -e "$pin_marker" ]; then