4343# VERSION & CONSTANTS
4444# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4545
46- __version__ = "5.3.2 "
46+ __version__ = "5.3.3 "
4747
4848REQUIRED_CHECK_KEYS = {"category" , "label" , "command" , "safe_pattern" , "level" , "description" }
4949
@@ -642,7 +642,8 @@ def idString(self) -> str:
642642class SshDevice (Device ):
643643 """Execute commands on a device over SSH (paramiko)."""
644644
645- def __init__ (self , host : str , port : int , user : str , password : str ):
645+ def __init__ (self , host : str , port : int , user : str , password : str ,
646+ trust_on_first_use : bool = False ):
646647 try :
647648 import paramiko
648649 except Exception :
@@ -657,12 +658,40 @@ def __init__(self, host: str, port: int, user: str, password: str):
657658
658659 self .client = self .paramiko .SSHClient ()
659660 self .client .load_system_host_keys ()
660- self .client .set_missing_host_key_policy (self .paramiko .AutoAddPolicy ())
661+ if trust_on_first_use :
662+ # User explicitly opted in via --ssh-tofu. Print a clear warning
663+ # so this never happens by accident or unnoticed.
664+ print (
665+ f"{ Colors .YELLOW } ⚠ SSH host-key trust-on-first-use enabled (--ssh-tofu).{ Colors .RESET } \n "
666+ f"{ Colors .YELLOW } First connections will silently trust any host key; "
667+ f"vulnerable to MITM during the first handshake.{ Colors .RESET } \n "
668+ f"{ Colors .YELLOW } Only use this on controlled lab / CI networks.{ Colors .RESET } " ,
669+ file = sys .stderr ,
670+ )
671+ self .client .set_missing_host_key_policy (self .paramiko .AutoAddPolicy ())
672+ else :
673+ # Safe default: refuse to connect to hosts whose key is not
674+ # already in the user's known_hosts. Pre-populate with
675+ # 'ssh-keyscan -H host >> ~/.ssh/known_hosts' or pass --ssh-tofu.
676+ self .client .set_missing_host_key_policy (self .paramiko .RejectPolicy ())
661677 try :
662678 self .client .connect (hostname = host , port = port , username = user ,
663679 password = password , look_for_keys = False ,
664680 allow_agent = False , timeout = 20 )
665- except (paramiko .AuthenticationException , paramiko .SSHException , OSError ) as e :
681+ except self .paramiko .SSHException as e :
682+ msg = str (e )
683+ if "not found in known_hosts" in msg .lower () or "Server" in msg :
684+ print (
685+ f"ERROR: SSH host key for { host } :{ port } is not in known_hosts.\n "
686+ f" Either pre-populate it on the auditor machine:\n "
687+ f" ssh-keyscan -H -t ed25519,rsa { host } >> ~/.ssh/known_hosts\n "
688+ f" Or, if you accept the trust-on-first-use risk, pass --ssh-tofu." ,
689+ file = sys .stderr ,
690+ )
691+ else :
692+ print (f"ERROR: SSH connection failed: { e } " , file = sys .stderr )
693+ sys .exit (1 )
694+ except (paramiko .AuthenticationException , OSError ) as e :
666695 print (f"ERROR: SSH connection failed: { e } " , file = sys .stderr )
667696 sys .exit (1 )
668697
@@ -2082,6 +2111,10 @@ def main():
20822111 ap .add_argument ("--port" , type = int , default = 22 , help = "SSH port (or overridden by UART)" )
20832112 ap .add_argument ("--ssh-user" , help = "SSH username" )
20842113 ap .add_argument ("--ssh-pass" , help = "SSH password" )
2114+ ap .add_argument ("--ssh-tofu" , action = "store_true" ,
2115+ help = "SSH trust-on-first-use: silently accept and store unknown host keys. "
2116+ "Convenient for CI / lab networks auditing many fresh devices; "
2117+ "weakens the SSH MITM guarantee on the first connection. Off by default." )
20852118 ap .add_argument ("--uart-port" , help = "UART serial port (e.g. /dev/ttyUSB0, /dev/ttyS0, COM3)" )
20862119 ap .add_argument ("--baud" , type = int , default = 0 , help = "UART baud rate (0 = auto-detect, common: 115200, 9600)" )
20872120 ap .add_argument ("--out" , default = "hardax_output" , help = "Output directory" )
@@ -2159,7 +2192,8 @@ def main():
21592192 or os .environ .get ("HARDAX_SSH_PASS" )
21602193 or getpass .getpass (f"SSH password for { args .ssh_user } @{ args .host } : " )
21612194 )
2162- device = SshDevice (args .host , args .port , args .ssh_user , ssh_pass )
2195+ device = SshDevice (args .host , args .port , args .ssh_user , ssh_pass ,
2196+ trust_on_first_use = args .ssh_tofu )
21632197
21642198 else : # uart
21652199 if not args .uart_port :
0 commit comments