浏览代码

When connecting to new SSH host, its host key can be automatically accepted in scripting and .NET assembly

(cherry picked from commit b79f8c47891e64c415eac4f54da4dcf219c278a6)

Source commit: 6e2e836240f48d226a2e0af5cd92fb96a5c897bd
Martin Prikryl 5 年之前
父节点
当前提交
21d47649f0

+ 17 - 5
dotnet/Session.cs

@@ -1847,17 +1847,29 @@ namespace WinSCP
                 List<string> switches = new List<string>();
 
                 if (!string.IsNullOrEmpty(sessionOptions.SshHostKeyFingerprint) ||
-                    (sessionOptions.GiveUpSecurityAndAcceptAnySshHostKey && !scanFingerprint))
+                    ((sessionOptions.SshHostKeyPolicy != SshHostKeyPolicy.Check) && !scanFingerprint))
                 {
                     if (!sessionOptions.IsSsh)
                     {
-                        throw Logger.WriteException(new ArgumentException("SessionOptions.SshHostKeyFingerprint or SessionOptions.GiveUpSecurityAndAcceptAnySshHostKey is set, but SessionOptions.Protocol is neither Protocol.Sftp nor Protocol.Scp."));
+                        throw Logger.WriteException(new ArgumentException("SessionOptions.SshHostKeyFingerprint is set or sessionOptions.SshHostKeyPolicy has not the default value Check, but SessionOptions.Protocol is neither Protocol.Sftp nor Protocol.Scp."));
                     }
                     string sshHostKeyFingerprint = sessionOptions.SshHostKeyFingerprint;
-                    if (sessionOptions.GiveUpSecurityAndAcceptAnySshHostKey)
+                    switch (sessionOptions.SshHostKeyPolicy)
                     {
-                        Logger.WriteLine("WARNING! Giving up security and accepting any key as configured");
-                        sshHostKeyFingerprint = AddStarToList(sshHostKeyFingerprint);
+                        case SshHostKeyPolicy.Check:
+                            // noop
+                            break;
+                        case SshHostKeyPolicy.GiveUpSecurityAndAcceptAny:
+                            sshHostKeyFingerprint = AddStarToList(sshHostKeyFingerprint);
+                            Logger.WriteLine("WARNING! Giving up security and accepting any key as configured");
+                            break;
+                        case SshHostKeyPolicy.AcceptNew:
+                            if (!string.IsNullOrEmpty(sshHostKeyFingerprint))
+                            {
+                                throw Logger.WriteException(new ArgumentException("SessionOptions.SshHostKeyFingerprint is set and SshHostKeyPolicy is not Check"));
+                            }
+                            sshHostKeyFingerprint = "acceptnew";
+                            break;
                     }
                     switches.Add(FormatSwitch("hostkey", sshHostKeyFingerprint));
                 }

+ 23 - 2
dotnet/SessionOptions.cs

@@ -35,6 +35,15 @@ namespace WinSCP
         Explicit = 3,
     }
 
+    [Guid("8A98AB8F-30E8-4539-A3DE-A33DDC43B33C")]
+    [ComVisible(true)]
+    public enum SshHostKeyPolicy
+    {
+        Check = 0,
+        GiveUpSecurityAndAcceptAny = 1,
+        AcceptNew = 2,
+    }
+
     [Guid("2D4EF368-EE80-4C15-AE77-D12AEAF4B00A")]
     [ClassInterface(Constants.ClassInterface)]
     [ComVisible(true)]
@@ -63,7 +72,9 @@ namespace WinSCP
 
         // SSH
         public string SshHostKeyFingerprint { get { return _sshHostKeyFingerprint; } set { SetSshHostKeyFingerprint(value); } }
-        public bool GiveUpSecurityAndAcceptAnySshHostKey { get; set; }
+        public SshHostKeyPolicy SshHostKeyPolicy { get; set; }
+        [Obsolete("Use SshHostKeyPolicy")]
+        public bool GiveUpSecurityAndAcceptAnySshHostKey { get { return GetGiveUpSecurityAndAcceptAnySshHostKey(); } set { SetGiveUpSecurityAndAcceptAnySshHostKey(value); } }
         public string SshPrivateKeyPath { get; set; }
         [Obsolete("Use PrivateKeyPassphrase")]
         public string SshPrivateKeyPassphrase { get { return PrivateKeyPassphrase; } set { PrivateKeyPassphrase = value; } }
@@ -196,7 +207,7 @@ namespace WinSCP
             UserName = null;
             Password = null;
             SshHostKeyFingerprint = null;
-            GiveUpSecurityAndAcceptAnySshHostKey = false;
+            SshHostKeyPolicy = SshHostKeyPolicy.Check;
             TlsHostCertificateFingerprint = null;
             GiveUpSecurityAndAcceptAnyTlsHostCertificate = false;
             if (!string.IsNullOrEmpty(userInfo))
@@ -468,6 +479,16 @@ namespace WinSCP
             return Name;
         }
 
+        private void SetGiveUpSecurityAndAcceptAnySshHostKey(bool value)
+        {
+            SshHostKeyPolicy = value ? SshHostKeyPolicy.GiveUpSecurityAndAcceptAny : SshHostKeyPolicy.Check;
+        }
+
+        private bool GetGiveUpSecurityAndAcceptAnySshHostKey()
+        {
+            return (SshHostKeyPolicy == SshHostKeyPolicy.GiveUpSecurityAndAcceptAny);
+        }
+
         private SecureString _securePassword;
         private SecureString _secureNewPassword;
         private SecureString _securePrivateKeyPassphrase;

+ 6 - 1
source/core/Configuration.cpp

@@ -166,13 +166,18 @@ THierarchicalStorage * TConfiguration::CreateConfigStorage()
   return CreateScpStorage(SessionList);
 }
 //---------------------------------------------------------------------------
+THierarchicalStorage * TConfiguration::CreateConfigRegistryStorage()
+{
+  return new TRegistryStorage(RegistryStorageKey);
+}
+//---------------------------------------------------------------------------
 THierarchicalStorage * TConfiguration::CreateScpStorage(bool & SessionList)
 {
   TGuard Guard(FCriticalSection);
   THierarchicalStorage * Result;
   if (Storage == stRegistry)
   {
-    Result = new TRegistryStorage(RegistryStorageKey);
+    Result = CreateConfigRegistryStorage();
   }
   else if (Storage == stNul)
   {

+ 1 - 0
source/core/Configuration.h

@@ -245,6 +245,7 @@ public:
   void __fastcall RememberLastFingerprint(const UnicodeString & SiteKey, const UnicodeString & FingerprintType, const UnicodeString & Fingerprint);
   UnicodeString __fastcall LastFingerprint(const UnicodeString & SiteKey, const UnicodeString & FingerprintType);
   THierarchicalStorage * CreateConfigStorage();
+  THierarchicalStorage * CreateConfigRegistryStorage();
   virtual THierarchicalStorage * CreateScpStorage(bool & SessionList);
   void __fastcall TemporaryLogging(const UnicodeString ALogFileName);
   void __fastcall TemporaryActionsLogging(const UnicodeString ALogFileName);

+ 6 - 15
source/core/PuttyIntf.cpp

@@ -22,6 +22,7 @@ char appname_[50];
 const char *const appname = appname_;
 extern const bool share_can_be_downstream = false;
 extern const bool share_can_be_upstream = false;
+THierarchicalStorage * PuttyStorage = NULL;
 //---------------------------------------------------------------------------
 extern "C"
 {
@@ -409,7 +410,6 @@ TPuttyRegistryTypes PuttyRegistryTypes;
 static long OpenWinSCPKey(HKEY Key, const char * SubKey, HKEY * Result, bool CanCreate)
 {
   long R;
-  DebugAssert(Configuration != NULL);
 
   DebugAssert(Key == HKEY_CURRENT_USER);
   DebugUsedParam(Key);
@@ -431,19 +431,18 @@ static long OpenWinSCPKey(HKEY Key, const char * SubKey, HKEY * Result, bool Can
   }
   else
   {
-    // we expect this to be called only from verify_host_key() or store_host_key()
+    // we expect this to be called only from retrieve_host_key() or store_host_key()
     DebugAssert(RegKey == L"SshHostKeys");
 
-    THierarchicalStorage * Storage = Configuration->CreateConfigStorage();
-    Storage->AccessMode = (CanCreate ? smReadWrite : smRead);
-    if (Storage->OpenSubKey(RegKey, CanCreate))
+    DebugAssert(PuttyStorage != NULL);
+    DebugAssert(PuttyStorage->AccessMode == (CanCreate ? smReadWrite : smRead));
+    if (PuttyStorage->OpenSubKey(RegKey, CanCreate))
     {
-      *Result = reinterpret_cast<HKEY>(Storage);
+      *Result = reinterpret_cast<HKEY>(PuttyStorage);
       R = ERROR_SUCCESS;
     }
     else
     {
-      delete Storage;
       R = ERROR_CANTOPEN;
     }
   }
@@ -557,7 +556,6 @@ long reg_set_winscp_value_ex(HKEY Key, const char * ValueName, unsigned long Res
     return ERROR_SUCCESS;
   }
   DebugAssert(PuttyRegistryMode == prmRedirect);
-  DebugAssert(Configuration != NULL);
 
   DebugAssert(Type == REG_SZ);
   DebugUsedParam(Type);
@@ -583,13 +581,6 @@ long reg_close_winscp_key(HKEY Key)
     return ERROR_SUCCESS;
   }
   DebugAssert(PuttyRegistryMode == prmRedirect);
-  DebugAssert(Configuration != NULL);
-
-  THierarchicalStorage * Storage = reinterpret_cast<THierarchicalStorage *>(Key);
-  if (Storage != NULL)
-  {
-    delete Storage;
-  }
 
   return ERROR_SUCCESS;
 }

+ 2 - 0
source/core/PuttyIntf.h

@@ -40,4 +40,6 @@ struct ScpSeat : public Seat
   ScpSeat(TSecureShell * SecureShell);
 };
 //---------------------------------------------------------------------------
+extern THierarchicalStorage * PuttyStorage;
+//---------------------------------------------------------------------------
 #endif

+ 83 - 7
source/core/SecureShell.cpp

@@ -22,6 +22,7 @@
 #define MAX_BUFSIZE 32768
 //---------------------------------------------------------------------------
 const wchar_t HostKeyDelimiter = L';';
+static std::unique_ptr<TCriticalSection> PuttyStorageSection(TraceInitPtr(new TCriticalSection()));
 //---------------------------------------------------------------------------
 struct TPuttyTranslation
 {
@@ -2172,8 +2173,32 @@ void __fastcall TSecureShell::GetRealHost(UnicodeString & Host, int & Port)
   }
 }
 //---------------------------------------------------------------------------
-UnicodeString __fastcall TSecureShell::RetrieveHostKey(UnicodeString Host, int Port, const UnicodeString KeyType)
+bool TSecureShell::HaveAcceptNewHostKeyPolicy()
 {
+  return SameText(FSessionData->HostKey.Trim(), L"acceptnew");
+}
+//---------------------------------------------------------------------------
+THierarchicalStorage * TSecureShell::GetHostKeyStorage()
+{
+  if (!Configuration->Persistent && HaveAcceptNewHostKeyPolicy())
+  {
+    return Configuration->CreateConfigRegistryStorage();
+  }
+  else
+  {
+    return Configuration->CreateConfigStorage();
+  }
+}
+//---------------------------------------------------------------------------
+UnicodeString __fastcall TSecureShell::RetrieveHostKey(const UnicodeString & Host, int Port, const UnicodeString & KeyType)
+{
+  std::unique_ptr<THierarchicalStorage> Storage(GetHostKeyStorage());
+  Storage->AccessMode = smRead;
+  TGuard Guard(PuttyStorageSection.get());
+  DebugAssert(PuttyStorage == NULL);
+  TValueRestorer<THierarchicalStorage *> StorageRestorer(PuttyStorage);
+  PuttyStorage = Storage.get();
+
   AnsiString AnsiStoredKeys;
   AnsiStoredKeys.SetLength(10240);
   UnicodeString Result;
@@ -2327,6 +2352,19 @@ bool TSecureShell::VerifyCachedHostKey(
   return Result;
 }
 //---------------------------------------------------------------------------
+UnicodeString TSecureShell::StoreHostKey(
+  const UnicodeString & Host, int Port, const UnicodeString & KeyType, const UnicodeString & KeyStr)
+{
+  TGuard Guard(PuttyStorageSection.get());
+  DebugAssert(PuttyStorage == NULL);
+  TValueRestorer<THierarchicalStorage *> StorageRestorer(PuttyStorage);
+  std::unique_ptr<THierarchicalStorage> Storage(GetHostKeyStorage());
+  Storage->AccessMode = smReadWrite;
+  PuttyStorage = Storage.get();
+  store_host_key(AnsiString(Host).c_str(), Port, AnsiString(KeyType).c_str(), AnsiString(KeyStr).c_str());
+  return Storage->Source;
+}
+//---------------------------------------------------------------------------
 void __fastcall TSecureShell::VerifyHostKey(
   const UnicodeString & AHost, int Port, const UnicodeString & KeyType, const UnicodeString & KeyStr,
   const UnicodeString & Fingerprint)
@@ -2376,17 +2414,35 @@ void __fastcall TSecureShell::VerifyHostKey(
     Abort();
   }
 
-  bool Result = false;
+  bool AcceptNew = HaveAcceptNewHostKeyPolicy();
+  UnicodeString ConfigHostKey;
+  if (!AcceptNew)
+  {
+    ConfigHostKey = FSessionData->HostKey;
+  }
 
   UnicodeString StoredKeys = RetrieveHostKey(Host, Port, KeyType);
-  Result = VerifyCachedHostKey(StoredKeys, KeyStr, FingerprintMD5, FingerprintSHA256);
+  bool Result = VerifyCachedHostKey(StoredKeys, KeyStr, FingerprintMD5, FingerprintSHA256);
+  if (!Result && AcceptNew)
+  {
+    if (!StoredKeys.IsEmpty()) // optimization + avoiding the log message
+    {
+      AcceptNew = false;
+    }
+    else if (have_any_ssh2_hostkey(FSeat, AnsiString(Host).c_str(), Port))
+    {
+      LogEvent(L"Host key not found in the cache, but other key types found, cannot accept new key");
+      AcceptNew = false;
+    }
+  }
 
   bool ConfiguredKeyNotMatch = false;
 
-  if (!Result && !FSessionData->HostKey.IsEmpty() &&
+  if (!Result && !ConfigHostKey.IsEmpty() &&
+      // Should test have_any_ssh2_hostkey + No need to bother with AcceptNew, as we never get here
       (StoredKeys.IsEmpty() || FSessionData->OverrideCachedHostKey))
   {
-    UnicodeString Buf = FSessionData->HostKey;
+    UnicodeString Buf = ConfigHostKey;
     while (!Result && !Buf.IsEmpty())
     {
       UnicodeString ExpectedKey = CutToChar(Buf, HostKeyDelimiter, false);
@@ -2414,6 +2470,26 @@ void __fastcall TSecureShell::VerifyHostKey(
     }
   }
 
+  if (!Result && AcceptNew && DebugAlwaysTrue(ConfigHostKey.IsEmpty()))
+  {
+    try
+    {
+      UnicodeString StorageSource = StoreHostKey(Host, Port, KeyType, KeyStr);
+      UnicodeString StoredKeys = RetrieveHostKey(Host, Port, KeyType);
+      if (StoredKeys != KeyStr)
+      {
+        throw Exception(UnicodeString());
+      }
+      Configuration->Usage->Inc(L"HostKeyNewAccepted");
+      LogEvent(FORMAT(L"Warning: Stored new host key to %s - This should occur only on the first connection", (StorageSource)));
+      Result = true;
+    }
+    catch (Exception & E)
+    {
+      FUI->FatalError(&E, LoadStr(STORE_NEW_HOSTKEY_ERROR));
+    }
+  }
+
   if (!Result)
   {
     bool Verified;
@@ -2492,7 +2568,7 @@ void __fastcall TSecureShell::VerifyHostKey(
           StoreKeyStr = (StoredKeys + HostKeyDelimiter + StoreKeyStr);
           // fall thru
         case qaYes:
-          store_host_key(AnsiString(Host).c_str(), Port, AnsiString(KeyType).c_str(), AnsiString(StoreKeyStr).c_str());
+          StoreHostKey(Host, Port, KeyType, StoreKeyStr);
           Verified = true;
           break;
 
@@ -2513,7 +2589,7 @@ void __fastcall TSecureShell::VerifyHostKey(
       UnicodeString Message;
       if (ConfiguredKeyNotMatch)
       {
-        Message = FMTLOAD(CONFIGURED_KEY_NOT_MATCH, (FSessionData->HostKey));
+        Message = FMTLOAD(CONFIGURED_KEY_NOT_MATCH, (ConfigHostKey));
       }
       else if (!Configuration->Persistent && Configuration->Scripting)
       {

+ 5 - 1
source/core/SecureShell.h

@@ -102,9 +102,13 @@ private:
   bool __fastcall TryFtp();
   UnicodeString __fastcall ConvertInput(const RawByteString & Input);
   void __fastcall GetRealHost(UnicodeString & Host, int & Port);
-  UnicodeString __fastcall RetrieveHostKey(UnicodeString Host, int Port, const UnicodeString KeyType);
+  UnicodeString __fastcall RetrieveHostKey(const UnicodeString & Host, int Port, const UnicodeString & KeyType);
+  bool HaveAcceptNewHostKeyPolicy();
+  THierarchicalStorage * GetHostKeyStorage();
   bool VerifyCachedHostKey(
     const UnicodeString & StoredKeys, const UnicodeString & KeyStr, const UnicodeString & FingerprintMD5, const UnicodeString & FingerprintSHA256);
+  UnicodeString StoreHostKey(
+    const UnicodeString & Host, int Port, const UnicodeString & KeyType, const UnicodeString & KeyStr);
 
 protected:
   TCaptureOutputEvent FOnCaptureOutput;

+ 1 - 0
source/putty/puttyexp.h

@@ -23,6 +23,7 @@ void md5checksum(const char * buffer, int len, unsigned char output[16]);
 typedef const struct ssh_keyalg * cp_ssh_keyalg;
 void get_hostkey_algs(int * count, cp_ssh_keyalg * SignKeys);
 void get_macs(int * count, const struct ssh2_macalg *** amacs);
+int have_any_ssh2_hostkey(Seat * seat, const char * host, int port);
 
 // from wingss.c
 

+ 13 - 0
source/putty/ssh2transport.c

@@ -2086,3 +2086,16 @@ void get_macs(int * count, const struct ssh2_macalg *** amacs)
     *amacs = macs;
     *count = lenof(macs);
 }
+
+int have_any_ssh2_hostkey(Seat * seat, const char * host, int port)
+{
+    int j;
+    for (j = 0; j < lenof(ssh2_hostkey_algs); j++)
+    {
+        if (have_ssh_host_key(seat, host, port, ssh2_hostkey_algs[j].alg->cache_id))
+        {
+            return 1;
+        }
+    }
+    return 0;
+}

+ 1 - 0
source/resource/TextsCore.h

@@ -271,6 +271,7 @@
 #define UNKNOWN_FILE_ENCRYPTION 747
 #define INVALID_ENCRYPT_KEY     748
 #define UNREQUESTED_FILE        749
+#define STORE_NEW_HOSTKEY_ERROR 750
 
 #define CORE_CONFIRMATION_STRINGS 300
 #define CONFIRM_PROLONG_TIMEOUT3 301

+ 1 - 0
source/resource/TextsCore1.rc

@@ -242,6 +242,7 @@ BEGIN
   UNKNOWN_FILE_ENCRYPTION, "File is not encrypted using a known encryption."
   INVALID_ENCRYPT_KEY, "**Invalid encryption key.**\n\nEncryption key for %s encryption must have %d bytes. It must be entered in hexadecimal representation (i.e. %d characters)."
   UNREQUESTED_FILE, "Server sent a file that was not requested."
+  STORE_NEW_HOSTKEY_ERROR, "Failed to store new host key."
 
   CORE_CONFIRMATION_STRINGS, "CORE_CONFIRMATION"
   CONFIRM_PROLONG_TIMEOUT3, "Host is not communicating for %d seconds.\n\nWait for another %0:d seconds?"