Merge pull request #131 from naturallaw777/copilot/fix-sovran-legacy-security-check

Replace Python `crypt` module with `openssl passwd` (Python 3.13 compatibility)
This commit is contained in:
Sovran_Systems
2026-04-07 16:17:02 -05:00
committed by GitHub
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
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:
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

View File

@@ -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
# 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
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
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
# 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
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
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