Pārlūkot izejas kodu

Bug 1394: API/Method Session.ScanFingerprint to scan server's hostkey or certificate fingerprint

https://winscp.net/tracker/1394

Source commit: 485a5f31fce53e51009c606533c7b1f34fe257d7
Martin Prikryl 9 gadi atpakaļ
vecāks
revīzija
197c95a87b

+ 2 - 1
dotnet/GlobalSuppressions.cs

@@ -78,7 +78,7 @@ using System.Diagnostics.CodeAnalysis;
 [assembly: SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "WinSCP.Logger.#WriteLine(System.String)")]
 [assembly: SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Scope = "member", Target = "WinSCP.Logger.#WriteEnvironmentInfo()")]
 [assembly: SuppressMessage("Microsoft.Design", "CA1003:UseGenericEventHandlerInstances", Scope = "type", Target = "WinSCP.OutputDataReceivedEventHandler")]
-[assembly: SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Scope = "member", Target = "WinSCP.ExeSessionProcess.#.ctor(WinSCP.Session,System.String,System.String)")]
+[assembly: SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Scope = "member", Target = "WinSCP.ExeSessionProcess.#.ctor(WinSCP.Session,System.Boolean,System.String)")]
 [assembly: SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "WinSCP.ExeSessionProcess.#CreateEvent(System.String)")]
 [assembly: SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources", Scope = "member", Target = "WinSCP.ConsoleCommStruct.#_ptr")]
 [assembly: SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "WinSCP.ExeSessionProcess.#InitializeConsole()")]
@@ -158,3 +158,4 @@ using System.Diagnostics.CodeAnalysis;
 [assembly: SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "WinSCP.ExeSessionProcess.#CreateFileMapping(System.String)")]
 [assembly: SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "WinSCP.Logger.#CreateCounters()")]
 [assembly: SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "WinSCP.SessionLogReader.#LogContents()")]
+[assembly: SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope = "member", Target = "WinSCP.Session.#SessionOptionsToSwitches(WinSCP.SessionOptions,System.Boolean)")]

+ 91 - 20
dotnet/Session.cs

@@ -176,7 +176,7 @@ namespace WinSCP
                 {
                     SetupTempPath();
 
-                    _process = new ExeSessionProcess(this);
+                    _process = ExeSessionProcess.CreateForSession(this);
 
                     _process.OutputDataReceived += ProcessOutputDataReceived;
 
@@ -203,7 +203,10 @@ namespace WinSCP
 
                     string command;
                     string log;
-                    SessionOptionsToOpenCommand(sessionOptions, out command, out log);
+                    SessionOptionsToUrlAndSwitches(sessionOptions, false, out command, out log);
+                    const string openCommand = "open ";
+                    command = openCommand + command;
+                    log = openCommand + log;
                     WriteCommand(command, log);
 
                     string logExplanation =
@@ -281,6 +284,65 @@ namespace WinSCP
             }
         }
 
+        public string ScanFingerprint(SessionOptions sessionOptions)
+        {
+            using (Logger.CreateCallstackAndLock())
+            {
+                string result;
+
+                CheckNotDisposed();
+
+                if (Opened)
+                {
+                    throw new InvalidOperationException("Session is already opened");
+                }
+
+                try
+                {
+                    string command;
+                    string log; // unused
+                    SessionOptionsToUrlAndSwitches(sessionOptions, true, out command, out log);
+
+                    string additionalArguments = "/fingerprintscan " + command;
+
+                    _process = ExeSessionProcess.CreateForConsole(this, additionalArguments);
+
+                    _process.OutputDataReceived += ProcessOutputDataReceived;
+
+                    _process.Start();
+
+                    GotOutput();
+
+                    while (!_process.HasExited)
+                    {
+                        Thread.Sleep(50);
+
+                        CheckForTimeout();
+                    }
+
+
+                    string output = string.Join(Environment.NewLine, new List<string>(Output).ToArray());
+                    if (_process.ExitCode == 0)
+                    {
+                        result = output;
+                    }
+                    else
+                    {
+                        throw new SessionRemoteException(this, output);
+                    }
+
+                }
+                catch (Exception e)
+                {
+                    Logger.WriteLine("Exception: {0}", e);
+                    Cleanup();
+                    throw;
+                }
+
+                return result;
+            }
+        }
+
         public void Close()
         {
             using (Logger.CreateCallstackAndLock())
@@ -1160,7 +1222,7 @@ namespace WinSCP
             }
         }
 
-        private void SessionOptionsToOpenCommand(SessionOptions sessionOptions, out string command, out string log)
+        private void SessionOptionsToUrlAndSwitches(SessionOptions sessionOptions, bool scanFingerprint, out string command, out string log)
         {
             using (Logger.CreateCallstack())
             {
@@ -1202,16 +1264,24 @@ namespace WinSCP
                         throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "{0} is not supported", sessionOptions.Protocol));
                 }
 
-                bool hasUsername = !string.IsNullOrEmpty(sessionOptions.UserName);
-                if (hasUsername)
+                bool hasUsername;
+                if (!scanFingerprint)
+                {
+                    hasUsername = !string.IsNullOrEmpty(sessionOptions.UserName);
+                    if (hasUsername)
+                    {
+                        head += UriEscape(sessionOptions.UserName);
+                    }
+                }
+                else
                 {
-                    head += UriEscape(sessionOptions.UserName);
+                    hasUsername = false;
                 }
 
                 string url = head;
                 string logUrl = head;
 
-                if (sessionOptions.SecurePassword != null)
+                if ((sessionOptions.SecurePassword != null) && !scanFingerprint)
                 {
                     if (!hasUsername)
                     {
@@ -1242,7 +1312,7 @@ namespace WinSCP
                     tail += ":" + sessionOptions.PortNumber.ToString(CultureInfo.InvariantCulture);
                 }
 
-                if (!string.IsNullOrEmpty(sessionOptions.WebdavRoot))
+                if (!string.IsNullOrEmpty(sessionOptions.WebdavRoot) && !scanFingerprint)
                 {
                     if (sessionOptions.Protocol != Protocol.Webdav)
                     {
@@ -1255,7 +1325,7 @@ namespace WinSCP
                 url += tail;
                 logUrl += tail;
 
-                string arguments = SessionOptionsToOpenSwitches(sessionOptions);
+                string arguments = SessionOptionsToSwitches(sessionOptions, scanFingerprint);
 
                 Tools.AddRawParameters(ref arguments, sessionOptions.RawSettings, "-rawsettings");
 
@@ -1265,19 +1335,19 @@ namespace WinSCP
                 }
 
                 // Switches should (and particularly the -rawsettings MUST) come after the URL
-                command = "open \"" + Tools.ArgumentEscape(url) + "\"" + arguments;
-                log = "open \"" + Tools.ArgumentEscape(logUrl) + "\"" + arguments;
+                command = "\"" + Tools.ArgumentEscape(url) + "\"" + arguments;
+                log = "\"" + Tools.ArgumentEscape(logUrl) + "\"" + arguments;
             }
         }
 
-        private string SessionOptionsToOpenSwitches(SessionOptions sessionOptions)
+        private string SessionOptionsToSwitches(SessionOptions sessionOptions, bool scanFingerprint)
         {
             using (Logger.CreateCallstack())
             {
                 List<string> switches = new List<string>();
 
                 if (!string.IsNullOrEmpty(sessionOptions.SshHostKeyFingerprint) ||
-                    sessionOptions.GiveUpSecurityAndAcceptAnySshHostKey)
+                    (sessionOptions.GiveUpSecurityAndAcceptAnySshHostKey && !scanFingerprint))
                 {
                     if (!sessionOptions.IsSsh)
                     {
@@ -1293,13 +1363,13 @@ namespace WinSCP
                 }
                 else
                 {
-                    if (sessionOptions.IsSsh && DefaultConfigurationInternal)
+                    if (sessionOptions.IsSsh && DefaultConfigurationInternal && !scanFingerprint)
                     {
                         throw new ArgumentException("SessionOptions.Protocol is Protocol.Sftp or Protocol.Scp, but SessionOptions.SshHostKeyFingerprint is not set.");
                     }
                 }
 
-                if (!string.IsNullOrEmpty(sessionOptions.SshPrivateKeyPath))
+                if (!string.IsNullOrEmpty(sessionOptions.SshPrivateKeyPath) && !scanFingerprint)
                 {
                     if (!sessionOptions.IsSsh)
                     {
@@ -1308,7 +1378,7 @@ namespace WinSCP
                     switches.Add(FormatSwitch("privatekey", sessionOptions.SshPrivateKeyPath));
                 }
 
-                if (!string.IsNullOrEmpty(sessionOptions.TlsClientCertificatePath))
+                if (!string.IsNullOrEmpty(sessionOptions.TlsClientCertificatePath) && !scanFingerprint)
                 {
                     if (!sessionOptions.IsTls)
                     {
@@ -1317,7 +1387,7 @@ namespace WinSCP
                     switches.Add(FormatSwitch("clientcert", sessionOptions.TlsClientCertificatePath));
                 }
 
-                if (!string.IsNullOrEmpty(sessionOptions.PrivateKeyPassphrase))
+                if (!string.IsNullOrEmpty(sessionOptions.PrivateKeyPassphrase) && !scanFingerprint)
                 {
                     if (string.IsNullOrEmpty(sessionOptions.SshPrivateKeyPath) && string.IsNullOrEmpty(sessionOptions.TlsClientCertificatePath))
                     {
@@ -1348,8 +1418,9 @@ namespace WinSCP
                     }
                 }
 
-                if (!string.IsNullOrEmpty(sessionOptions.TlsHostCertificateFingerprint) ||
-                    sessionOptions.GiveUpSecurityAndAcceptAnyTlsHostCertificate)
+                if ((!string.IsNullOrEmpty(sessionOptions.TlsHostCertificateFingerprint) ||
+                     sessionOptions.GiveUpSecurityAndAcceptAnyTlsHostCertificate) &&
+                    !scanFingerprint)
                 {
                     if (!sessionOptions.IsTls)
                     {
@@ -1364,7 +1435,7 @@ namespace WinSCP
                     switches.Add(FormatSwitch("certificate", tlsHostCertificateFingerprint));
                 }
 
-                if (sessionOptions.Protocol == Protocol.Ftp)
+                if ((sessionOptions.Protocol == Protocol.Ftp) && !scanFingerprint)
                 {
                     switches.Add(FormatSwitch("passive", (sessionOptions.FtpMode == FtpMode.Passive)));
                 }

+ 27 - 3
dotnet/internal/ExeSessionProcess.cs

@@ -20,7 +20,17 @@ namespace WinSCP
         public bool HasExited { get { return _process.HasExited; } }
         public int ExitCode { get { return _process.ExitCode; } }
 
-        public ExeSessionProcess(Session session)
+        public static ExeSessionProcess CreateForSession(Session session)
+        {
+            return new ExeSessionProcess(session, true, null);
+        }
+
+        public static ExeSessionProcess CreateForConsole(Session session, string additionalArguments)
+        {
+            return new ExeSessionProcess(session, false, additionalArguments);
+        }
+
+        private ExeSessionProcess(Session session, bool useXmlLog, string additionalArguments)
         {
             _session = session;
             _logger = session.Logger;
@@ -64,7 +74,15 @@ namespace WinSCP
                     logSwitch = string.Format(CultureInfo.InvariantCulture, "/log=\"{0}\" ", LogPathEscape(_session.SessionLogPath));
                 }
 
-                string xmlLogSwitch = string.Format(CultureInfo.InvariantCulture, "/xmllog=\"{0}\" ", LogPathEscape(_session.XmlLogPath));
+                string xmlLogSwitch;
+                if (useXmlLog)
+                {
+                    xmlLogSwitch = string.Format(CultureInfo.InvariantCulture, "/xmllog=\"{0}\" /xmlgroups ", LogPathEscape(_session.XmlLogPath));
+                }
+                else
+                {
+                    xmlLogSwitch = "";
+                }
 
                 string assemblyVersionStr =
                     (assemblyVersion == null) ? "unk" :
@@ -74,11 +92,16 @@ namespace WinSCP
                     string.Format(CultureInfo.InvariantCulture, "/dotnet={0} ", assemblyVersionStr);
 
                 string arguments =
-                    xmlLogSwitch + "/xmlgroups /nointeractiveinput " + assemblyVersionSwitch +
+                    xmlLogSwitch + "/nointeractiveinput " + assemblyVersionSwitch +
                     configSwitch + logSwitch + _session.AdditionalExecutableArguments;
 
                 Tools.AddRawParameters(ref arguments, _session.RawConfiguration, "/rawconfig");
 
+                if (!string.IsNullOrEmpty(additionalArguments))
+                {
+                    arguments += " " + additionalArguments;
+                }
+
                 _process = new Process();
                 _process.StartInfo.FileName = executablePath;
                 _process.StartInfo.WorkingDirectory = Path.GetDirectoryName(executablePath);
@@ -120,6 +143,7 @@ namespace WinSCP
         {
             using (_logger.CreateCallstack())
             {
+                // The /console is redundant for CreateForConsole
                 _process.StartInfo.Arguments += string.Format(CultureInfo.InvariantCulture, " /console /consoleinstance={0}", _instanceName);
 
                 // When running under IIS in "impersonated" mode, the process starts, but does not do anything.

+ 179 - 172
source/core/FtpFileSystem.cpp

@@ -439,7 +439,7 @@ void __fastcall TFTPFileSystem::Open()
 
     // ask for username if it was not specified in advance, even on retry,
     // but keep previous one as default,
-    if (Data->UserNameExpanded.IsEmpty())
+    if (Data->UserNameExpanded.IsEmpty() && !FTerminal->SessionData->FingerprintScan)
     {
       FTerminal->LogEvent(L"Username prompt (no username provided)");
 
@@ -4114,203 +4114,210 @@ bool __fastcall TFTPFileSystem::HandleAsynchRequestVerifyCertificate(
     FSessionInfo.CertificateFingerprint =
       BytesToHex(RawByteString((const char*)Data.Hash, Data.HashLen), false, L':');
 
-    UnicodeString CertificateSubject = Data.Subject.Organization;
-    FTerminal->LogEvent(FORMAT(L"Verifying certificate for \"%s\" with fingerprint %s and %d failures", (CertificateSubject, FSessionInfo.CertificateFingerprint, Data.VerificationResult)));
-
-    bool VerificationResult = false;
-    bool TryWindowsSystemCertificateStore = false;
-    UnicodeString VerificationResultStr;
-    switch (Data.VerificationResult)
+    if (FTerminal->SessionData->FingerprintScan)
     {
-      case X509_V_OK:
-        VerificationResult = true;
-        break;
-      case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT:
-        VerificationResultStr = LoadStr(CERT_ERR_UNABLE_TO_GET_ISSUER_CERT);
-        TryWindowsSystemCertificateStore = true;
-        break;
-      case X509_V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE:
-        VerificationResultStr = LoadStr(CERT_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE);
-        break;
-      case X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY:
-        VerificationResultStr = LoadStr(CERT_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY);
-        break;
-      case X509_V_ERR_CERT_SIGNATURE_FAILURE:
-        VerificationResultStr = LoadStr(CERT_ERR_CERT_SIGNATURE_FAILURE);
-        break;
-      case X509_V_ERR_CERT_NOT_YET_VALID:
-        VerificationResultStr = LoadStr(CERT_ERR_CERT_NOT_YET_VALID);
-        break;
-      case X509_V_ERR_CERT_HAS_EXPIRED:
-        VerificationResultStr = LoadStr(CERT_ERR_CERT_HAS_EXPIRED);
-        break;
-      case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD:
-        VerificationResultStr = LoadStr(CERT_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD);
-        break;
-      case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD:
-        VerificationResultStr = LoadStr(CERT_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD);
-        break;
-      case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT:
-        VerificationResultStr = LoadStr(CERT_ERR_DEPTH_ZERO_SELF_SIGNED_CERT);
-        TryWindowsSystemCertificateStore = true;
-        break;
-      case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN:
-        VerificationResultStr = LoadStr(CERT_ERR_SELF_SIGNED_CERT_IN_CHAIN);
-        TryWindowsSystemCertificateStore = true;
-        break;
-      case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:
-        VerificationResultStr = LoadStr(CERT_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY);
-        TryWindowsSystemCertificateStore = true;
-        break;
-      case X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE:
-        VerificationResultStr = LoadStr(CERT_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE);
-        TryWindowsSystemCertificateStore = true;
-        break;
-      case X509_V_ERR_INVALID_CA:
-        VerificationResultStr = LoadStr(CERT_ERR_INVALID_CA);
-        break;
-      case X509_V_ERR_PATH_LENGTH_EXCEEDED:
-        VerificationResultStr = LoadStr(CERT_ERR_PATH_LENGTH_EXCEEDED);
-        break;
-      case X509_V_ERR_INVALID_PURPOSE:
-        VerificationResultStr = LoadStr(CERT_ERR_INVALID_PURPOSE);
-        break;
-      case X509_V_ERR_CERT_UNTRUSTED:
-        VerificationResultStr = LoadStr(CERT_ERR_CERT_UNTRUSTED);
-        TryWindowsSystemCertificateStore = true;
-        break;
-      case X509_V_ERR_CERT_REJECTED:
-        VerificationResultStr = LoadStr(CERT_ERR_CERT_REJECTED);
-        break;
-      case X509_V_ERR_KEYUSAGE_NO_CERTSIGN:
-        VerificationResultStr = LoadStr(CERT_ERR_KEYUSAGE_NO_CERTSIGN);
-        break;
-      case X509_V_ERR_CERT_CHAIN_TOO_LONG:
-        VerificationResultStr = LoadStr(CERT_ERR_CERT_CHAIN_TOO_LONG);
-        break;
-      default:
-        VerificationResultStr =
-          FORMAT(L"%s (%s)",
-            (LoadStr(CERT_ERR_UNKNOWN), X509_verify_cert_error_string(Data.VerificationResult)));
-        break;
+      RequestResult = 0;
     }
-
-    // TryWindowsSystemCertificateStore is set for the same set of failures
-    // as trigger NE_SSL_UNTRUSTED flag in ne_openssl.c's verify_callback()
-    if (!VerificationResult && TryWindowsSystemCertificateStore)
+    else
     {
-      if (WindowsValidateCertificate(Data.Certificate, Data.CertificateLen))
+      UnicodeString CertificateSubject = Data.Subject.Organization;
+      FTerminal->LogEvent(FORMAT(L"Verifying certificate for \"%s\" with fingerprint %s and %d failures", (CertificateSubject, FSessionInfo.CertificateFingerprint, Data.VerificationResult)));
+
+      bool VerificationResult = false;
+      bool TryWindowsSystemCertificateStore = false;
+      UnicodeString VerificationResultStr;
+      switch (Data.VerificationResult)
       {
-        FTerminal->LogEvent(L"Certificate verified against Windows certificate store");
-        VerificationResult = true;
+        case X509_V_OK:
+          VerificationResult = true;
+          break;
+        case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT:
+          VerificationResultStr = LoadStr(CERT_ERR_UNABLE_TO_GET_ISSUER_CERT);
+          TryWindowsSystemCertificateStore = true;
+          break;
+        case X509_V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE:
+          VerificationResultStr = LoadStr(CERT_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE);
+          break;
+        case X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY:
+          VerificationResultStr = LoadStr(CERT_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY);
+          break;
+        case X509_V_ERR_CERT_SIGNATURE_FAILURE:
+          VerificationResultStr = LoadStr(CERT_ERR_CERT_SIGNATURE_FAILURE);
+          break;
+        case X509_V_ERR_CERT_NOT_YET_VALID:
+          VerificationResultStr = LoadStr(CERT_ERR_CERT_NOT_YET_VALID);
+          break;
+        case X509_V_ERR_CERT_HAS_EXPIRED:
+          VerificationResultStr = LoadStr(CERT_ERR_CERT_HAS_EXPIRED);
+          break;
+        case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD:
+          VerificationResultStr = LoadStr(CERT_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD);
+          break;
+        case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD:
+          VerificationResultStr = LoadStr(CERT_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD);
+          break;
+        case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT:
+          VerificationResultStr = LoadStr(CERT_ERR_DEPTH_ZERO_SELF_SIGNED_CERT);
+          TryWindowsSystemCertificateStore = true;
+          break;
+        case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN:
+          VerificationResultStr = LoadStr(CERT_ERR_SELF_SIGNED_CERT_IN_CHAIN);
+          TryWindowsSystemCertificateStore = true;
+          break;
+        case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:
+          VerificationResultStr = LoadStr(CERT_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY);
+          TryWindowsSystemCertificateStore = true;
+          break;
+        case X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE:
+          VerificationResultStr = LoadStr(CERT_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE);
+          TryWindowsSystemCertificateStore = true;
+          break;
+        case X509_V_ERR_INVALID_CA:
+          VerificationResultStr = LoadStr(CERT_ERR_INVALID_CA);
+          break;
+        case X509_V_ERR_PATH_LENGTH_EXCEEDED:
+          VerificationResultStr = LoadStr(CERT_ERR_PATH_LENGTH_EXCEEDED);
+          break;
+        case X509_V_ERR_INVALID_PURPOSE:
+          VerificationResultStr = LoadStr(CERT_ERR_INVALID_PURPOSE);
+          break;
+        case X509_V_ERR_CERT_UNTRUSTED:
+          VerificationResultStr = LoadStr(CERT_ERR_CERT_UNTRUSTED);
+          TryWindowsSystemCertificateStore = true;
+          break;
+        case X509_V_ERR_CERT_REJECTED:
+          VerificationResultStr = LoadStr(CERT_ERR_CERT_REJECTED);
+          break;
+        case X509_V_ERR_KEYUSAGE_NO_CERTSIGN:
+          VerificationResultStr = LoadStr(CERT_ERR_KEYUSAGE_NO_CERTSIGN);
+          break;
+        case X509_V_ERR_CERT_CHAIN_TOO_LONG:
+          VerificationResultStr = LoadStr(CERT_ERR_CERT_CHAIN_TOO_LONG);
+          break;
+        default:
+          VerificationResultStr =
+            FORMAT(L"%s (%s)",
+              (LoadStr(CERT_ERR_UNKNOWN), X509_verify_cert_error_string(Data.VerificationResult)));
+          break;
       }
-    }
 
-    UnicodeString Summary;
-    if (!VerificationResult)
-    {
-      Summary = VerificationResultStr + L" " + FMTLOAD(CERT_ERRDEPTH, (Data.VerificationDepth + 1));
-    }
+      // TryWindowsSystemCertificateStore is set for the same set of failures
+      // as trigger NE_SSL_UNTRUSTED flag in ne_openssl.c's verify_callback()
+      if (!VerificationResult && TryWindowsSystemCertificateStore)
+      {
+        if (WindowsValidateCertificate(Data.Certificate, Data.CertificateLen))
+        {
+          FTerminal->LogEvent(L"Certificate verified against Windows certificate store");
+          VerificationResult = true;
+        }
+      }
 
-    if (IsIPAddress(FTerminal->SessionData->HostNameExpanded))
-    {
-      VerificationResult = false;
-      AddToList(Summary, FMTLOAD(CERT_IP_CANNOT_VERIFY, (FTerminal->SessionData->HostNameExpanded)), L"\n\n");
-    }
-    else if (!VerifyCertificateHostName(Data))
-    {
-      VerificationResult = false;
-      AddToList(Summary, FMTLOAD(CERT_NAME_MISMATCH, (FTerminal->SessionData->HostNameExpanded)), L"\n\n");
-    }
+      UnicodeString Summary;
+      if (!VerificationResult)
+      {
+        Summary = VerificationResultStr + L" " + FMTLOAD(CERT_ERRDEPTH, (Data.VerificationDepth + 1));
+      }
 
-    if (VerificationResult)
-    {
-      Summary = LoadStr(CERT_OK);
-    }
+      if (IsIPAddress(FTerminal->SessionData->HostNameExpanded))
+      {
+        VerificationResult = false;
+        AddToList(Summary, FMTLOAD(CERT_IP_CANNOT_VERIFY, (FTerminal->SessionData->HostNameExpanded)), L"\n\n");
+      }
+      else if (!VerifyCertificateHostName(Data))
+      {
+        VerificationResult = false;
+        AddToList(Summary, FMTLOAD(CERT_NAME_MISMATCH, (FTerminal->SessionData->HostNameExpanded)), L"\n\n");
+      }
 
-    FSessionInfo.Certificate =
-      FMTLOAD(CERT_TEXT, (
-        FormatContact(Data.Issuer),
-        FormatContact(Data.Subject),
-        FormatValidityTime(Data.ValidFrom),
-        FormatValidityTime(Data.ValidUntil),
-        FSessionInfo.CertificateFingerprint,
-        Summary));
+      if (VerificationResult)
+      {
+        Summary = LoadStr(CERT_OK);
+      }
 
-    RequestResult = 0;
+      FSessionInfo.Certificate =
+        FMTLOAD(CERT_TEXT, (
+          FormatContact(Data.Issuer),
+          FormatContact(Data.Subject),
+          FormatValidityTime(Data.ValidFrom),
+          FormatValidityTime(Data.ValidUntil),
+          FSessionInfo.CertificateFingerprint,
+          Summary));
 
-    if (VerificationResult)
-    {
-      RequestResult = 1;
-    }
+      RequestResult = 0;
 
-    UnicodeString SiteKey = FTerminal->SessionData->SiteKey;
-    if (RequestResult == 0)
-    {
-      if (FTerminal->VerifyCertificate(CertificateStorageKey, SiteKey,
-            FSessionInfo.CertificateFingerprint, CertificateSubject, Data.VerificationResult))
+      if (VerificationResult)
       {
         RequestResult = 1;
       }
-    }
 
-    if (RequestResult == 0)
-    {
-      TClipboardHandler ClipboardHandler;
-      ClipboardHandler.Text = FSessionInfo.CertificateFingerprint;
-
-      TQueryButtonAlias Aliases[1];
-      Aliases[0].Button = qaRetry;
-      Aliases[0].Alias = LoadStr(COPY_KEY_BUTTON);
-      Aliases[0].OnClick = &ClipboardHandler.Copy;
-
-      TQueryParams Params(qpWaitInBatch);
-      Params.HelpKeyword = HELP_VERIFY_CERTIFICATE;
-      Params.NoBatchAnswers = qaYes | qaRetry;
-      Params.Aliases = Aliases;
-      Params.AliasesCount = LENOF(Aliases);
-      unsigned int Answer = FTerminal->QueryUser(
-        FMTLOAD(VERIFY_CERT_PROMPT3, (FSessionInfo.Certificate)),
-        NULL, qaYes | qaNo | qaCancel | qaRetry, &Params, qtWarning);
-
-      switch (Answer)
+      UnicodeString SiteKey = FTerminal->SessionData->SiteKey;
+      if (RequestResult == 0)
       {
-        case qaYes:
-          // 2 = always, as used by FZ's VerifyCertDlg.cpp,
-          // however FZAPI takes all non-zero values equally
-          RequestResult = 2;
-          break;
-
-        case qaNo:
+        if (FTerminal->VerifyCertificate(CertificateStorageKey, SiteKey,
+              FSessionInfo.CertificateFingerprint, CertificateSubject, Data.VerificationResult))
+        {
           RequestResult = 1;
-          break;
+        }
+      }
 
-        case qaCancel:
-          FTerminal->Configuration->Usage->Inc(L"HostNotVerified");
-          RequestResult = 0;
-          break;
+      if (RequestResult == 0)
+      {
+        TClipboardHandler ClipboardHandler;
+        ClipboardHandler.Text = FSessionInfo.CertificateFingerprint;
+
+        TQueryButtonAlias Aliases[1];
+        Aliases[0].Button = qaRetry;
+        Aliases[0].Alias = LoadStr(COPY_KEY_BUTTON);
+        Aliases[0].OnClick = &ClipboardHandler.Copy;
+
+        TQueryParams Params(qpWaitInBatch);
+        Params.HelpKeyword = HELP_VERIFY_CERTIFICATE;
+        Params.NoBatchAnswers = qaYes | qaRetry;
+        Params.Aliases = Aliases;
+        Params.AliasesCount = LENOF(Aliases);
+        unsigned int Answer = FTerminal->QueryUser(
+          FMTLOAD(VERIFY_CERT_PROMPT3, (FSessionInfo.Certificate)),
+          NULL, qaYes | qaNo | qaCancel | qaRetry, &Params, qtWarning);
+
+        switch (Answer)
+        {
+          case qaYes:
+            // 2 = always, as used by FZ's VerifyCertDlg.cpp,
+            // however FZAPI takes all non-zero values equally
+            RequestResult = 2;
+            break;
 
-        default:
-          DebugFail();
-          RequestResult = 0;
-          break;
+          case qaNo:
+            RequestResult = 1;
+            break;
+
+          case qaCancel:
+            FTerminal->Configuration->Usage->Inc(L"HostNotVerified");
+            RequestResult = 0;
+            break;
+
+          default:
+            DebugFail();
+            RequestResult = 0;
+            break;
+        }
+
+        if (RequestResult == 2)
+        {
+          FTerminal->CacheCertificate(
+            CertificateStorageKey, SiteKey,
+            FSessionInfo.CertificateFingerprint, Data.VerificationResult);
+        }
       }
 
-      if (RequestResult == 2)
+      // Cache only if the certificate was not automatically accepted
+      if (!VerificationResult && (RequestResult != 0))
       {
-        FTerminal->CacheCertificate(
-          CertificateStorageKey, SiteKey,
-          FSessionInfo.CertificateFingerprint, Data.VerificationResult);
+        FTerminal->Configuration->RememberLastFingerprint(
+          FTerminal->SessionData->SiteKey, TlsFingerprintType, FSessionInfo.CertificateFingerprint);
       }
     }
 
-    // Cache only if the certificate was not automatically accepted
-    if (!VerificationResult && (RequestResult != 0))
-    {
-      FTerminal->Configuration->RememberLastFingerprint(
-        FTerminal->SessionData->SiteKey, TlsFingerprintType, FSessionInfo.CertificateFingerprint);
-    }
-
     return true;
   }
 }

+ 11 - 0
source/core/SecureShell.cpp

@@ -124,6 +124,11 @@ const TSessionInfo & __fastcall TSecureShell::GetSessionInfo()
   return FSessionInfo;
 }
 //---------------------------------------------------------------------
+UnicodeString __fastcall TSecureShell::GetHostKeyFingerprint()
+{
+  return FSessionInfo.HostKeyFingerprint;
+}
+//---------------------------------------------------------------------
 Conf * __fastcall TSecureShell::StoreToConfig(TSessionData * Data, bool Simple)
 {
   Conf * conf = conf_new();
@@ -2101,6 +2106,12 @@ void __fastcall TSecureShell::VerifyHostKey(UnicodeString Host, int Port,
   GetRealHost(Host, Port);
 
   FSessionInfo.HostKeyFingerprint = Fingerprint;
+
+  if (FSessionData->FingerprintScan)
+  {
+    Abort();
+  }
+
   UnicodeString NormalizedFingerprint = NormalizeFingerprint(Fingerprint);
 
   bool Result = false;

+ 1 - 0
source/core/SecureShell.h

@@ -131,6 +131,7 @@ public:
   void __fastcall SendNull();
 
   const TSessionInfo & __fastcall GetSessionInfo();
+  UnicodeString __fastcall GetHostKeyFingerprint();
   bool __fastcall SshFallbackCmd() const;
   unsigned long __fastcall MaxPacketSize();
   void __fastcall ClearStdError();

+ 2 - 0
source/core/SessionData.cpp

@@ -136,6 +136,7 @@ void __fastcall TSessionData::Default()
   SendBuf = DefaultSendBuf;
   SshSimple = true;
   HostKey = L"";
+  FingerprintScan = false;
   FOverrideCachedHostKey = true;
   Note = L"";
 
@@ -288,6 +289,7 @@ void __fastcall TSessionData::NonPersistant()
   PROPERTY(RekeyData); \
   PROPERTY(RekeyTime); \
   PROPERTY(HostKey); \
+  PROPERTY(FingerprintScan); \
   \
   PROPERTY(UpdateDirectories); \
   PROPERTY(CacheDirectories); \

+ 2 - 0
source/core/SessionData.h

@@ -196,6 +196,7 @@ private:
   bool FIsWorkspace;
   UnicodeString FLink;
   UnicodeString FHostKey;
+  bool FFingerprintScan;
   bool FOverrideCachedHostKey;
   UnicodeString FNote;
 
@@ -575,6 +576,7 @@ public:
   __property bool IsWorkspace = { read = FIsWorkspace, write = SetIsWorkspace };
   __property UnicodeString Link = { read = FLink, write = SetLink };
   __property UnicodeString HostKey = { read = FHostKey, write = SetHostKey };
+  __property bool FingerprintScan = { read = FFingerprintScan, write = FFingerprintScan };
   __property bool OverrideCachedHostKey = { read = FOverrideCachedHostKey };
   __property UnicodeString Note = { read = FNote, write = SetNote };
   __property UnicodeString StorageKey = { read = GetStorageKey };

+ 30 - 0
source/core/Terminal.cpp

@@ -864,6 +864,27 @@ void __fastcall TTerminal::ResetConnection()
   // as they can still be referenced in the GUI atm
 }
 //---------------------------------------------------------------------------
+UnicodeString __fastcall TTerminal::FingerprintScan()
+{
+  SessionData->FingerprintScan = true;
+  try
+  {
+    Open();
+    // we should never get here
+    Abort();
+  }
+  catch (...)
+  {
+    if (!FFingerprintScanned.IsEmpty())
+    {
+      return FFingerprintScanned;
+    }
+    throw;
+  }
+  DebugFail();
+  return UnicodeString();
+}
+//---------------------------------------------------------------------------
 void __fastcall TTerminal::Open()
 {
   TAutoNestingCounter OpeningCounter(FOpening);
@@ -944,6 +965,10 @@ void __fastcall TTerminal::Open()
                 catch(Exception & E)
                 {
                   DebugAssert(!FSecureShell->Active);
+                  if (SessionData->FingerprintScan)
+                  {
+                    FFingerprintScanned = FSecureShell->GetHostKeyFingerprint();
+                  }
                   if (!FSecureShell->Active && !FTunnelError.IsEmpty())
                   {
                     // the only case where we expect this to happen
@@ -1025,6 +1050,11 @@ void __fastcall TTerminal::Open()
           delete FDirectoryChangesCache;
           FDirectoryChangesCache = NULL;
         }
+        if (SessionData->FingerprintScan && (FFileSystem != NULL) &&
+            DebugAlwaysTrue(SessionData->Ftps != ftpsNone))
+        {
+          FFingerprintScanned = FFileSystem->GetSessionInfo().CertificateFingerprint;
+        }
         throw;
       }
     }

+ 2 - 0
source/core/Terminal.h

@@ -206,6 +206,7 @@ private:
   bool FRememberedPasswordTried;
   bool FRememberedTunnelPasswordTried;
   int FNesting;
+  UnicodeString FFingerprintScanned;
 
   void __fastcall CommandError(Exception * E, const UnicodeString Msg);
   unsigned int __fastcall CommandError(Exception * E, const UnicodeString Msg,
@@ -398,6 +399,7 @@ public:
   __fastcall ~TTerminal();
   void __fastcall Open();
   void __fastcall Close();
+  UnicodeString __fastcall FingerprintScan();
   void __fastcall Reopen(int Params);
   virtual void __fastcall DirectoryModified(const UnicodeString Path, bool SubDirs);
   virtual void __fastcall DirectoryLoaded(TRemoteFileList * FileList);

+ 80 - 71
source/core/WebDAVFileSystem.cpp

@@ -2163,99 +2163,108 @@ void __fastcall TWebDAVFileSystem::SinkFile(const UnicodeString FileName,
 //---------------------------------------------------------------------------
 bool TWebDAVFileSystem::VerifyCertificate(const TWebDAVCertificateData & Data)
 {
-  FTerminal->LogEvent(
-    FORMAT(L"Verifying certificate for \"%s\" with fingerprint %s and %2.2X failures",
-      (Data.Subject, Data.Fingerprint, Data.Failures)));
-
-  int Failures = Data.Failures;
-  if (NeonWindowsValidateCertificate(Failures, Data.AsciiCert))
-  {
-    FTerminal->LogEvent(L"Certificate verified against Windows certificate store");
-  }
-
-  UnicodeString Summary;
+  FSessionInfo.CertificateFingerprint = Data.Fingerprint;
 
-  if (Failures == 0)
+  bool Result;
+  if (FTerminal->SessionData->FingerprintScan)
   {
-    Summary = LoadStr(CERT_OK);
+    Result = false;
   }
   else
   {
-    Summary = NeonCertificateFailuresErrorStr(Failures, FHostName);
-  }
+    FTerminal->LogEvent(
+      FORMAT(L"Verifying certificate for \"%s\" with fingerprint %s and %2.2X failures",
+        (Data.Subject, Data.Fingerprint, Data.Failures)));
 
-  UnicodeString ValidityTimeFormat = L"ddddd tt";
-  FSessionInfo.CertificateFingerprint = Data.Fingerprint;
-  FSessionInfo.Certificate =
-    FMTLOAD(CERT_TEXT, (
-      Data.Issuer + L"\n",
-      Data.Subject + L"\n",
-      FormatDateTime(ValidityTimeFormat, Data.ValidFrom),
-      FormatDateTime(ValidityTimeFormat, Data.ValidUntil),
-      Data.Fingerprint,
-      Summary));
+    int Failures = Data.Failures;
+    if (NeonWindowsValidateCertificate(Failures, Data.AsciiCert))
+    {
+      FTerminal->LogEvent(L"Certificate verified against Windows certificate store");
+    }
 
-  bool Result = (Failures == 0);
+    UnicodeString Summary;
 
-  if (!Result)
-  {
-    UnicodeString SiteKey = TSessionData::FormatSiteKey(FHostName, FPortNumber);
-    if (!Result)
+    if (Failures == 0)
     {
-      Result = FTerminal->VerifyCertificate(
-        CertificateStorageKey, SiteKey, Data.Fingerprint, Data.Subject, Failures);
+      Summary = LoadStr(CERT_OK);
     }
+    else
+    {
+      Summary = NeonCertificateFailuresErrorStr(Failures, FHostName);
+    }
+
+    UnicodeString ValidityTimeFormat = L"ddddd tt";
+    FSessionInfo.Certificate =
+      FMTLOAD(CERT_TEXT, (
+        Data.Issuer + L"\n",
+        Data.Subject + L"\n",
+        FormatDateTime(ValidityTimeFormat, Data.ValidFrom),
+        FormatDateTime(ValidityTimeFormat, Data.ValidUntil),
+        Data.Fingerprint,
+        Summary));
+
+    Result = (Failures == 0);
 
     if (!Result)
     {
-      TClipboardHandler ClipboardHandler;
-      ClipboardHandler.Text = Data.Fingerprint;
-
-      TQueryButtonAlias Aliases[1];
-      Aliases[0].Button = qaRetry;
-      Aliases[0].Alias = LoadStr(COPY_KEY_BUTTON);
-      Aliases[0].OnClick = &ClipboardHandler.Copy;
-
-      TQueryParams Params;
-      Params.HelpKeyword = HELP_VERIFY_CERTIFICATE;
-      Params.NoBatchAnswers = qaYes | qaRetry;
-      Params.Aliases = Aliases;
-      Params.AliasesCount = LENOF(Aliases);
-      unsigned int Answer = FTerminal->QueryUser(
-        FMTLOAD(VERIFY_CERT_PROMPT3, (FSessionInfo.Certificate)),
-        NULL, qaYes | qaNo | qaCancel | qaRetry, &Params, qtWarning);
-      switch (Answer)
+      UnicodeString SiteKey = TSessionData::FormatSiteKey(FHostName, FPortNumber);
+      if (!Result)
+      {
+        Result = FTerminal->VerifyCertificate(
+          CertificateStorageKey, SiteKey, Data.Fingerprint, Data.Subject, Failures);
+      }
+
+      if (!Result)
+      {
+        TClipboardHandler ClipboardHandler;
+        ClipboardHandler.Text = Data.Fingerprint;
+
+        TQueryButtonAlias Aliases[1];
+        Aliases[0].Button = qaRetry;
+        Aliases[0].Alias = LoadStr(COPY_KEY_BUTTON);
+        Aliases[0].OnClick = &ClipboardHandler.Copy;
+
+        TQueryParams Params;
+        Params.HelpKeyword = HELP_VERIFY_CERTIFICATE;
+        Params.NoBatchAnswers = qaYes | qaRetry;
+        Params.Aliases = Aliases;
+        Params.AliasesCount = LENOF(Aliases);
+        unsigned int Answer = FTerminal->QueryUser(
+          FMTLOAD(VERIFY_CERT_PROMPT3, (FSessionInfo.Certificate)),
+          NULL, qaYes | qaNo | qaCancel | qaRetry, &Params, qtWarning);
+        switch (Answer)
+        {
+          case qaYes:
+            FTerminal->CacheCertificate(CertificateStorageKey, SiteKey, Data.Fingerprint, Failures);
+            Result = true;
+            break;
+
+          case qaNo:
+            Result = true;
+            break;
+
+          default:
+            DebugFail();
+          case qaCancel:
+            FTerminal->Configuration->Usage->Inc(L"HostNotVerified");
+            Result = false;
+            break;
+        }
+      }
+
+      if (Result)
       {
-        case qaYes:
-          FTerminal->CacheCertificate(CertificateStorageKey, SiteKey, Data.Fingerprint, Failures);
-          Result = true;
-          break;
-
-        case qaNo:
-          Result = true;
-          break;
-
-        default:
-          DebugFail();
-        case qaCancel:
-          FTerminal->Configuration->Usage->Inc(L"HostNotVerified");
-          Result = false;
-          break;
+        FTerminal->Configuration->RememberLastFingerprint(
+          FTerminal->SessionData->SiteKey, TlsFingerprintType, FSessionInfo.CertificateFingerprint);
       }
     }
 
     if (Result)
     {
-      FTerminal->Configuration->RememberLastFingerprint(
-        FTerminal->SessionData->SiteKey, TlsFingerprintType, FSessionInfo.CertificateFingerprint);
+      CollectTLSSessionInfo();
     }
   }
 
-  if (Result)
-  {
-    CollectTLSSessionInfo();
-  }
-
   return Result;
 }
 //------------------------------------------------------------------------------

+ 1 - 0
source/resource/TextsWin.h

@@ -83,6 +83,7 @@
 #define UPDATE_TOO_LOW          1197
 #define TIPS_NONE               1198
 #define KEYGEN_PUBLIC           1199
+#define FINGERPRINTSCAN_NEED_SECURE_SESSION 1200
 
 #define WIN_CONFIRMATION_STRINGS 1300
 #define CONFIRM_OVERWRITE_SESSION 1301

+ 1 - 0
source/resource/TextsWin1.rc

@@ -89,6 +89,7 @@ BEGIN
         UPDATE_TOO_LOW, "Your donation is below the limit required to enable automatic updates."
         TIPS_NONE, "No tips."
         KEYGEN_PUBLIC, "Converting public keys is not supported."
+        FINGERPRINTSCAN_NEED_SECURE_SESSION, "Secure session (SSH or TLS/SSL) not specified."
 
         WIN_CONFIRMATION_STRINGS, "WIN_CONFIRMATION"
         CONFIRM_OVERWRITE_SESSION, "Site with name '%s' already exists. Overwrite?"

+ 73 - 23
source/windows/ConsoleRunner.cpp

@@ -39,6 +39,21 @@ void TrimNewLine(UnicodeString & Str)
   }
 }
 //---------------------------------------------------------------------------
+static bool __fastcall ExceptionConsoleMessage(Exception * E, UnicodeString & Message)
+{
+  bool Result = ExceptionMessage(E, Message);
+  if (Result)
+  {
+    Message += L"\n";
+    ExtException * EE = dynamic_cast<ExtException *>(E);
+    if ((EE != NULL) && (EE->MoreMessages != NULL))
+    {
+      Message += EE->MoreMessages->Text + L"\n";
+    }
+  }
+  return Result;
+}
+//---------------------------------------------------------------------------
 class TConsole
 {
 public:
@@ -1741,15 +1756,10 @@ void __fastcall TConsoleRunner::DoShowException(TTerminal * Terminal, Exception
   }
 
   UnicodeString Message;
-  if (ExceptionMessage(E, Message))
+  if (ExceptionConsoleMessage(E, Message))
   {
     FCommandError = true;
     PrintMessage(Message);
-    ExtException * EE = dynamic_cast<ExtException *>(E);
-    if ((EE != NULL) && (EE->MoreMessages != NULL))
-    {
-      PrintMessage(EE->MoreMessages->Text);
-    }
   }
 
   TTerminal * LoggingTerminal = Terminal;
@@ -2222,6 +2232,16 @@ void __fastcall BatchSettings(TConsole * Console, TProgramParams * Params)
   }
 }
 //---------------------------------------------------------------------------
+static int __fastcall HandleException(TConsole * Console, Exception & E)
+{
+  UnicodeString Message;
+  if (ExceptionConsoleMessage(&E, Message))
+  {
+    Console->Print(Message);
+  }
+  return RESULT_ANY_ERROR;
+}
+//---------------------------------------------------------------------------
 bool __fastcall FindPuttygenCompatibleSwitch(
   TProgramParams * Params, const UnicodeString & Name, const UnicodeString & PuttygenName, UnicodeString & Value, bool & Set)
 {
@@ -2375,17 +2395,7 @@ int __fastcall KeyGen(TConsole * Console, TProgramParams * Params)
   }
   catch (Exception & E)
   {
-    UnicodeString Message;
-    if (ExceptionMessage(&E, Message))
-    {
-      ConsolePrintLine(Console, Message);
-      ExtException * EE = dynamic_cast<ExtException *>(&E);
-      if ((EE != NULL) && (EE->MoreMessages != NULL))
-      {
-        ConsolePrintLine(Console, EE->MoreMessages->Text);
-      }
-    }
-    Result = RESULT_ANY_ERROR;
+    Result = HandleException(Console, E);
   }
 
   Shred(Passphrase);
@@ -2395,6 +2405,43 @@ int __fastcall KeyGen(TConsole * Console, TProgramParams * Params)
   return Result;
 }
 //---------------------------------------------------------------------------
+int __fastcall FingerprintScan(TConsole * Console, TProgramParams * Params)
+{
+  int Result = RESULT_SUCCESS;
+  try
+  {
+    CheckLogParam(Params);
+
+    std::unique_ptr<TSessionData> SessionData;
+
+    if (Params->ParamCount > 0)
+    {
+      UnicodeString SessionUrl = Params->Param[1];
+      bool DefaultsOnly;
+      SessionData.reset(StoredSessions->ParseUrl(SessionUrl, Params, DefaultsOnly));
+      if (DefaultsOnly || !SessionData->CanLogin ||
+          (!SessionData->UsesSsh && (SessionData->Ftps == ftpsNone)))
+      {
+        SessionData.reset(NULL);
+      }
+    }
+
+    if (!SessionData)
+    {
+      throw Exception(LoadStr(FINGERPRINTSCAN_NEED_SECURE_SESSION));
+    }
+
+    std::unique_ptr<TTerminal> Terminal(new TTerminal(SessionData.get(), Configuration));
+    ConsolePrintLine(Console, Terminal->FingerprintScan());
+  }
+  catch (Exception & E)
+  {
+    Result = HandleException(Console, E);
+  }
+
+  return Result;
+}
+//---------------------------------------------------------------------------
 int __fastcall Console(TConsoleMode Mode)
 {
   DebugAssert(Mode != cmNone);
@@ -2447,6 +2494,14 @@ int __fastcall Console(TConsoleMode Mode)
         Result = KeyGen(Console, Params);
       }
     }
+    else if (Mode == cmFingerprintScan)
+    {
+      if (CheckSafe(Params))
+      {
+        Configuration->Usage->Inc(L"FingerprintScan");
+        Result = FingerprintScan(Console, Params);
+      }
+    }
     else
     {
       Runner = new TConsoleRunner(Console);
@@ -2486,12 +2541,7 @@ int __fastcall Console(TConsoleMode Mode)
         bool DefaultsOnly;
         delete StoredSessions->ParseUrl(Session, Params, DefaultsOnly);
 
-        UnicodeString LogFile;
-        if (Params->FindSwitch(LOG_SWITCH, LogFile) && CheckSafe(Params))
-        {
-          Configuration->Usage->Inc(L"ScriptLog");
-          Configuration->TemporaryLogging(LogFile);
-        }
+        CheckLogParam(Params);
         CheckXmlLogParam(Params);
 
         Result = Runner->Run(Session, Params,

+ 10 - 0
source/windows/UserInterface.cpp

@@ -1267,6 +1267,16 @@ void __fastcall MessageWithNoHelp(const UnicodeString & Message)
   }
 }
 //---------------------------------------------------------------------------
+void __fastcall CheckLogParam(TProgramParams * Params)
+{
+  UnicodeString LogFile;
+  if (Params->FindSwitch(LOG_SWITCH, LogFile) && CheckSafe(Params))
+  {
+    Configuration->Usage->Inc(L"ScriptLog");
+    Configuration->TemporaryLogging(LogFile);
+  }
+}
+//---------------------------------------------------------------------------
 bool __fastcall CheckXmlLogParam(TProgramParams * Params)
 {
   UnicodeString LogFile;

+ 3 - 1
source/windows/WinInterface.h

@@ -36,6 +36,7 @@ const int mpAllowContinueOnError = 0x02;
 #define KEYGEN_CHANGE_PASSPHRASE_SWITCH L"ChangePassphrase"
 #define LOG_SWITCH L"Log"
 #define INI_SWITCH L"Ini"
+#define FINGERPRINTSCAN_SWITCH L"FingerprintScan"
 
 struct TMessageParams
 {
@@ -84,6 +85,7 @@ void __fastcall MessageWithNoHelp(const UnicodeString & Message);
 
 class TProgramParams;
 bool __fastcall CheckSafe(TProgramParams * Params);
+void __fastcall CheckLogParam(TProgramParams * Params);
 bool __fastcall CheckXmlLogParam(TProgramParams * Params);
 
 UnicodeString __fastcall GetToolbarsLayoutStr(TComponent * OwnerComponent);
@@ -384,7 +386,7 @@ void __fastcall InsertPanelToMessageDialog(TCustomForm * Form, TPanel * Panel);
 void __fastcall NavigateMessageDialogToUrl(TCustomForm * Form, const UnicodeString & Url);
 
 // windows\Console.cpp
-enum TConsoleMode { cmNone, cmScripting, cmHelp, cmBatchSettings, cmKeyGen };
+enum TConsoleMode { cmNone, cmScripting, cmHelp, cmBatchSettings, cmKeyGen, cmFingerprintScan };
 int __fastcall Console(TConsoleMode Mode);
 
 // forms\EditorPreferences.cpp

+ 4 - 0
source/windows/WinMain.cpp

@@ -661,6 +661,10 @@ int __fastcall Execute()
   {
     Mode = cmKeyGen;
   }
+  else if (Params->FindSwitch(FINGERPRINTSCAN_SWITCH))
+  {
+    Mode = cmFingerprintScan;
+  }
   // We have to check for /console only after the other options,
   // as the /console is always used when we are run by winscp.com
   // (ambiguous use to pass console version)