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>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-07 21:11:13 +00:00
committed by GitHub
parent c872f1c6b0
commit 742f680d0d
2 changed files with 80 additions and 21 deletions

View File

@@ -2970,9 +2970,13 @@ def _is_free_password_default() -> bool:
password changes made via GNOME, passwd, or any method other than the Hub password changes made via GNOME, passwd, or any method other than the Hub
are detected correctly. 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"] 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: try:
with open("/etc/shadow", "r") as f: with open("/etc/shadow", "r") as f:
for line in f: for line in f:
@@ -2981,9 +2985,34 @@ def _is_free_password_default() -> bool:
current_hash = parts[1] current_hash = parts[1]
if not current_hash or current_hash in ("!", "*", "!!"): if not current_hash or current_hash in ("!", "*", "!!"):
return True # locked/no password — treat as default 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: for default_pw in FACTORY_DEFAULTS:
if crypt.crypt(default_pw, current_hash) == current_hash: 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 return True
except Exception:
return True # if openssl fails, assume default for safety
return False return False
except (FileNotFoundError, PermissionError): except (FileNotFoundError, PermissionError):
pass pass

View File

@@ -99,7 +99,7 @@ in
Type = "oneshot"; Type = "oneshot";
RemainAfterExit = true; 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 = '' script = ''
# Idempotency check # Idempotency check
if [ -f /var/lib/sovran-factory-sealed ]; then if [ -f /var/lib/sovran-factory-sealed ]; then
@@ -129,15 +129,30 @@ in
if [ -f /etc/shadow ]; then if [ -f /etc/shadow ]; then
FREE_HASH=$(grep '^free:' /etc/shadow | cut -d: -f2) FREE_HASH=$(grep '^free:' /etc/shadow | cut -d: -f2)
if [ -n "$FREE_HASH" ] && [ "$FREE_HASH" != "!" ] && [ "$FREE_HASH" != "*" ]; then 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 STILL_DEFAULT=false
# 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 for DEFAULT_PW in "free" "gosovransystems"; do
EXPECTED=$(DEFAULT_PW="$DEFAULT_PW" FREE_HASH="$FREE_HASH" python3 -c \ case "$ALGO_ID" in
"import crypt, os; print(crypt.crypt(os.environ['DEFAULT_PW'], os.environ['FREE_HASH']))") 6) EXPECTED=$(openssl passwd -6 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;;
if [ "$EXPECTED" = "$FREE_HASH" ]; then 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 STILL_DEFAULT=true
break break
fi fi
done done
fi
if [ "$STILL_DEFAULT" = "false" ]; then if [ "$STILL_DEFAULT" = "false" ]; then
echo "sovran-auto-seal: password has been changed from factory defaults live system detected. Restoring flag and exiting." echo "sovran-auto-seal: password has been changed from factory defaults live system detected. Restoring flag and exiting."
touch /var/lib/sovran-factory-sealed touch /var/lib/sovran-factory-sealed
@@ -209,7 +224,7 @@ in
Type = "oneshot"; Type = "oneshot";
RemainAfterExit = true; RemainAfterExit = true;
}; };
path = [ pkgs.coreutils pkgs.python3 ]; path = [ pkgs.coreutils pkgs.openssl ];
script = '' script = ''
# If sealed AND onboarded fully clean, nothing to do # If sealed AND onboarded fully clean, nothing to do
[ -f /var/lib/sovran-factory-sealed ] && [ -f /var/lib/sovran-customer-onboarded ] && exit 0 [ -f /var/lib/sovran-factory-sealed ] && [ -f /var/lib/sovran-customer-onboarded ] && exit 0
@@ -234,15 +249,30 @@ EOF
if [ -f /etc/shadow ]; then if [ -f /etc/shadow ]; then
FREE_HASH=$(grep '^free:' /etc/shadow | cut -d: -f2) FREE_HASH=$(grep '^free:' /etc/shadow | cut -d: -f2)
if [ -n "$FREE_HASH" ] && [ "$FREE_HASH" != "!" ] && [ "$FREE_HASH" != "*" ]; then 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 STILL_DEFAULT=false
# 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 for DEFAULT_PW in "free" "gosovransystems"; do
EXPECTED=$(DEFAULT_PW="$DEFAULT_PW" FREE_HASH="$FREE_HASH" python3 -c \ case "$ALGO_ID" in
"import crypt, os; print(crypt.crypt(os.environ['DEFAULT_PW'], os.environ['FREE_HASH']))") 6) EXPECTED=$(openssl passwd -6 -salt "$SALT" "$DEFAULT_PW" 2>/dev/null) ;;
if [ "$EXPECTED" = "$FREE_HASH" ]; then 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 STILL_DEFAULT=true
break break
fi fi
done done
fi
if [ "$STILL_DEFAULT" = "false" ]; then if [ "$STILL_DEFAULT" = "false" ]; then
# Password was changed clear any legacy warning and exit # Password was changed clear any legacy warning and exit
rm -f /var/lib/sovran/security-status /var/lib/sovran/security-warning rm -f /var/lib/sovran/security-status /var/lib/sovran/security-warning