From c872f1c6b00fc73e0b74497c700d3e6c685552b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:04:58 +0000 Subject: [PATCH 1/2] Initial plan From 742f680d0dcda88126def09db4e35ca1ad8adb9f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:11:13 +0000 Subject: [PATCH 2/2] fix: replace Python crypt module with openssl passwd for Python 3.13 compatibility Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/9544e3d5-f7f8-4299-9198-3b5f1f835d14 Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com> --- app/sovran_systemsos_web/server.py | 35 ++++++++++++++-- modules/core/factory-seal.nix | 66 ++++++++++++++++++++++-------- 2 files changed, 80 insertions(+), 21 deletions(-) diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index 41e1ff6..747df5c 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -2970,9 +2970,13 @@ def _is_free_password_default() -> bool: password changes made via GNOME, passwd, or any method other than the Hub are detected correctly. """ - import crypt # available in Python stdlib (deprecated in 3.13 but present on NixOS) + import subprocess + import re as _re FACTORY_DEFAULTS = ["free", "gosovransystems"] + # Map shadow algorithm IDs to openssl passwd flags (SHA-512 and SHA-256 only, + # matching the shell-script counterpart in factory-seal.nix) + ALGO_FLAGS = {"6": "-6", "5": "-5"} try: with open("/etc/shadow", "r") as f: for line in f: @@ -2981,9 +2985,34 @@ def _is_free_password_default() -> bool: current_hash = parts[1] if not current_hash or current_hash in ("!", "*", "!!"): return True # locked/no password — treat as default + # Parse hash: $id$[rounds=N$]salt$hash + hash_fields = current_hash.split("$") + # hash_fields: ["", id, salt_or_rounds, ...] + if len(hash_fields) < 4: + return True # unrecognized format — assume default for safety + algo_id = hash_fields[1] + salt_field = hash_fields[2] + if algo_id not in ALGO_FLAGS: + return True # unrecognized algorithm — assume default for safety + if salt_field.startswith("rounds="): + return True # can't extract real salt simply — assume default for safety + # Validate salt contains only safe characters (alphanumeric, '.', '/', '-', '_') + # to guard against unexpected shadow file content before passing to subprocess + if not _re.fullmatch(r"[A-Za-z0-9./\-_]+", salt_field): + return True # unexpected salt format — assume default for safety + openssl_flag = ALGO_FLAGS[algo_id] for default_pw in FACTORY_DEFAULTS: - if crypt.crypt(default_pw, current_hash) == current_hash: - return True + try: + result = subprocess.run( + ["openssl", "passwd", openssl_flag, "-salt", salt_field, default_pw], + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0 and result.stdout.strip() == current_hash: + return True + except Exception: + return True # if openssl fails, assume default for safety return False except (FileNotFoundError, PermissionError): pass diff --git a/modules/core/factory-seal.nix b/modules/core/factory-seal.nix index 5794558..0e4df9c 100644 --- a/modules/core/factory-seal.nix +++ b/modules/core/factory-seal.nix @@ -99,7 +99,7 @@ in Type = "oneshot"; RemainAfterExit = true; }; - path = [ pkgs.coreutils pkgs.e2fsprogs pkgs.python3 pkgs.postgresql pkgs.mariadb pkgs.shadow ]; + path = [ pkgs.coreutils pkgs.e2fsprogs pkgs.openssl pkgs.postgresql pkgs.mariadb pkgs.shadow ]; script = '' # ── Idempotency check ───────────────────────────────────────── if [ -f /var/lib/sovran-factory-sealed ]; then @@ -129,15 +129,30 @@ in if [ -f /etc/shadow ]; then FREE_HASH=$(grep '^free:' /etc/shadow | cut -d: -f2) if [ -n "$FREE_HASH" ] && [ "$FREE_HASH" != "!" ] && [ "$FREE_HASH" != "*" ]; then + ALGO_ID=$(printf '%s' "$FREE_HASH" | cut -d'$' -f2) + SALT=$(printf '%s' "$FREE_HASH" | cut -d'$' -f3) STILL_DEFAULT=false - for DEFAULT_PW in "free" "gosovransystems"; do - EXPECTED=$(DEFAULT_PW="$DEFAULT_PW" FREE_HASH="$FREE_HASH" python3 -c \ - "import crypt, os; print(crypt.crypt(os.environ['DEFAULT_PW'], os.environ['FREE_HASH']))") - if [ "$EXPECTED" = "$FREE_HASH" ]; then - STILL_DEFAULT=true - break - fi - done + # If the salt field starts with "rounds=", we cannot extract the real salt + # with a simple cut — treat as still-default for safety + if printf '%s' "$SALT" | grep -q '^rounds='; then + STILL_DEFAULT=true + else + for DEFAULT_PW in "free" "gosovransystems"; do + case "$ALGO_ID" in + 6) EXPECTED=$(openssl passwd -6 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;; + 5) EXPECTED=$(openssl passwd -5 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;; + *) + # Unknown hash algorithm — treat as still-default for safety + STILL_DEFAULT=true + break + ;; + esac + if [ -n "$EXPECTED" ] && [ "$EXPECTED" = "$FREE_HASH" ]; then + STILL_DEFAULT=true + break + fi + done + fi if [ "$STILL_DEFAULT" = "false" ]; then echo "sovran-auto-seal: password has been changed from factory defaults — live system detected. Restoring flag and exiting." touch /var/lib/sovran-factory-sealed @@ -209,7 +224,7 @@ in Type = "oneshot"; RemainAfterExit = true; }; - path = [ pkgs.coreutils pkgs.python3 ]; + path = [ pkgs.coreutils pkgs.openssl ]; script = '' # If sealed AND onboarded — fully clean, nothing to do [ -f /var/lib/sovran-factory-sealed ] && [ -f /var/lib/sovran-customer-onboarded ] && exit 0 @@ -234,15 +249,30 @@ EOF if [ -f /etc/shadow ]; then FREE_HASH=$(grep '^free:' /etc/shadow | cut -d: -f2) if [ -n "$FREE_HASH" ] && [ "$FREE_HASH" != "!" ] && [ "$FREE_HASH" != "*" ]; then + ALGO_ID=$(printf '%s' "$FREE_HASH" | cut -d'$' -f2) + SALT=$(printf '%s' "$FREE_HASH" | cut -d'$' -f3) STILL_DEFAULT=false - for DEFAULT_PW in "free" "gosovransystems"; do - EXPECTED=$(DEFAULT_PW="$DEFAULT_PW" FREE_HASH="$FREE_HASH" python3 -c \ - "import crypt, os; print(crypt.crypt(os.environ['DEFAULT_PW'], os.environ['FREE_HASH']))") - if [ "$EXPECTED" = "$FREE_HASH" ]; then - STILL_DEFAULT=true - break - fi - done + # If the salt field starts with "rounds=", we cannot extract the real salt + # with a simple cut — treat as still-default for safety + if printf '%s' "$SALT" | grep -q '^rounds='; then + STILL_DEFAULT=true + else + for DEFAULT_PW in "free" "gosovransystems"; do + case "$ALGO_ID" in + 6) EXPECTED=$(openssl passwd -6 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;; + 5) EXPECTED=$(openssl passwd -5 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;; + *) + # Unknown hash algorithm — treat as still-default for safety + STILL_DEFAULT=true + break + ;; + esac + if [ -n "$EXPECTED" ] && [ "$EXPECTED" = "$FREE_HASH" ]; then + STILL_DEFAULT=true + break + fi + done + fi if [ "$STILL_DEFAULT" = "false" ]; then # Password was changed — clear any legacy warning and exit rm -f /var/lib/sovran/security-status /var/lib/sovran/security-warning