FluentFTP icon indicating copy to clipboard operation
FluentFTP copied to clipboard

Make TLS1.3 connections work with AutoConnect

Open robinrodricks opened this issue 3 years ago • 4 comments

By @JosHuybrighs:

I did some further TLS1.3 tests against my Synology FTP server, added the master GitHub version of the FluentFTP project to my Visual Studio solution, and observed the following:

  • TLS session negotiation (Client Hello, Server Hello, Keys and Certificate exchange, ..) is always successfull.
  • When m_stream.ActivateEncryption returns, there is still 86 bytes of unread data in m_stream.
  • The next Execute("USER " + userName) then fails with 'Stale Data'.

I took a look at the stream data of the session negotation using Wireshark, and see there that the FTP server responds with 4 application data records, immediately followed by 1 other application data record. I am not sure, but what I understand from TLS 1.3 is that the first 4 records are used for the session negotiation. I debugged the code for checking stale data and what I see is that the unread data corresponds with the 5th application data record as I see it in Wireshark.

When I configure _ftpClient.StaleDataCheck = false, the connection is successfull and further data transfer (list folders, etc.) is OK. So it looks like the data is required and I assume it will, after the session activation, be consumed by sslstream (sChannel). So, my question: is the check for 'Stale Data' correct in FluentFTP when using TLS 1.3? The reason why I ask is because I saw this code in ConnectModule.cs:

#if NET50_OR_LATER
			if (protocol == SysSslProtocols.Tls13) {
				client.StaleDataCheck = false;
			}
			else {
				client.StaleDataCheck = true;
			}
#endif

I am using .NET6

robinrodricks avatar Aug 21 '22 08:08 robinrodricks

Does this work? FluentFTP.39.4.0-BETA1.zip

Try these:

  • Connect with manual config for TLS1.3
  • AutoConnect with no config settings

I have fully disabled the stale data check for all connection types during the Connect procedure.


39.4.0

  • New: Add SslProtocolActive property to retrieve the negotiated SSL/TLS protocol version
  • Fix: Support connecting to TLS 1.3 servers using AutoConnect (.NET 5.0+)

robinrodricks avatar Aug 21 '22 08:08 robinrodricks

@robinrodricks Thanks for the quick reaction and the SslProtocolActive property. I did a test with the 39.4.0-BETA1 .net6 dll and these are the results:

1. Connect with manual config and TLS1.3 works The code I used to test 'normal' connect is:

        public StorageDeviceOpenResult Open()
        {
            _certTrusted = true;
            ConditionalFileLogger.Log($"FTPStorageDevice.Open - Host: {Name}, port: {Port}, account: {AccountName}, connectionMode: {FTPConnectionMode}, tlsProtocol: {SslProtocol.ToString()}");

            var result = new StorageDeviceOpenResult() { Status = ConnectStatus.DriveNotAvailable };
            if (string.IsNullOrEmpty(AccountName) ||
                string.IsNullOrEmpty(Password))
            {
                result.Status = ConnectStatus.CouldNotAuthenticate;
                return result;
            }

            try
            {
                // Setup FtpClient
                NetworkCredential credential = new NetworkCredential(AccountName, Password);
                FtpTrace.EnableTracing = false;
                FtpTrace.LogToConsole = false;
                _ftpClient = new FtpClient(Host, Port, credential);
                if (ConditionalFileLogger.Level == ConditionalFileLogger.LoggingLevel.Detail)
                {
                    FtpTrace.EnableTracing = true;
                    _ftpClient.OnLogEvent = OnFTPLogEvent;
                }
                var offset = DateTimeOffset.Now.Offset;
                _ftpClient.TimeZone = 2; // Time zone of server should (with LIST) and must (with MLSD) be in UTC
                _ftpClient.TimeConversion = FtpDate.ServerTime;
                if (FTPConnectionMode != 2)
                {
                    // 1. Setup encryption mode
                    _ftpClient.EncryptionMode = FTPConnectionMode == 0 ? FtpEncryptionMode.Explicit : FtpEncryptionMode.Implicit;

                    // 2. Setup ssl/tls protocol
                    // If SslProtocol == TlsProtocol.LetOsDecide then let .NET sslstream pick the highest and most relevant
                    // TLS protocol (it will propose TLS 1.2 and TLS 1.3).
                    _ftpClient.SslProtocols = SslProtocol == TlsProtocol.LetOsDecide ? SslProtocols.None : SslProtocols.Tls12;

                    // 3. Prepare certificate handling
                    _certValidationPending = true;
                    _ftpClient.ValidateAnyCertificate = true;
                    _ftpClient.ValidateCertificate += _ftpClient_ValidateCertificate;

                    // 4. Get all trusted certificates in _trustedCerts (will be used in _ftpClient_ValidateCertificate)
                    var folder = Windows.Storage.ApplicationData.Current.LocalFolder;
                    string filePath = $"{folder.Path}\\trusted_certs.json";
                    if (File.Exists(filePath))
                    {
                        ConditionalFileLogger.Log("FTPStorageDevice.Initialize - Extract trusted certificates list");
                        DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(List<TrustedCert>));
                        using (FileStream fs = File.OpenRead(filePath))
                        {
                            _trustedCerts = (List<TrustedCert>)serializer.ReadObject(fs);
                            fs.Close();
                        }
                        ConditionalFileLogger.Log($"FTPStorageDevice.Initialize - File contains {_trustedCerts.Count} entries");
                    }
                    else
                    {
                        ConditionalFileLogger.Log("FTPStorageDevice.Initialize - There is no trusted certificates list");
                    }
                }

                // Connect to the server
                _ftpClient.Connect();
                _certValidationPending = false;
                if (_ftpClient.IsConnected)
                {
                    result.Status = ConnectStatus.Connected;
                    SelectedSslProtocol = SelectedSslProcolToTlsProtocol(_ftpClient.SslProtocolActive);
                    FtpRootDirectory = _ftpClient.GetWorkingDirectory();
                    ConditionalFileLogger.Log("FTPStorageDevice.Initialize - SUCCESS");
                }
                else
                {
                    ConditionalFileLogger.Log("FTPStorageDevice.Initialize - NOT CONNECTED");
                }
            }
            catch (FtpSecurityNotAvailableException e1)
            {
                result.Status = ConnectStatus.TLSNotSupported;
                result.ErrorCode = e1.HResult;
                result.ErrorMessage = e1.Message;
                ConditionalFileLogger.Log($"FTPStorageDevice.Initialize - FAIL-1, ErrorCode: {e1.HResult}, {e1.Message}");
            }
            catch (FtpAuthenticationException e2)
            {
                result.Status = e2.CompletionCode.Equals("421") ? ConnectStatus.DriveNotAvailable : ConnectStatus.AccountNotRegisteredAnymore; // AuthenticateRequestResult.SignInState.ServiceNotAvailable : AuthenticateRequestResult.SignInState.AccountNotRegistered;
                result.ErrorCode = e2.HResult;
                result.ErrorMessage = e2.Message;
                ConditionalFileLogger.Log($"FTPStorageDevice.Initialize - FAIL-2, ErrorCode: {e2.HResult}, {e2.Message}");
            }
            catch (AuthenticationException e3)
            {
                result.Status = _certTrusted ? ConnectStatus.AccountNotRegisteredAnymore : ConnectStatus.UntrustedCertificate; //  AuthenticateRequestResult.SignInState.AccountNotRegistered : AuthenticateRequestResult.SignInState.CertificateError;
                result.ErrorCode = e3.HResult;
                result.ErrorMessage = e3.Message;
                ConditionalFileLogger.Log($"FTPStorageDevice.Initialize - FAIL-3, ErrorCode: {e3.HResult}, {e3.Message}");
            }
            catch (Exception ex)
            {
                // 0x80131515: tls problem
                // 0x80131500: tls problem or wrong address
                // 0x80004004: wrong address
                if (FTPConnectionMode != 2 &&
                    (ex.HResult & 0xFFFF00) == 0x131500)
                {
                    result.Status = _certTrusted ? ConnectStatus.CouldNotAuthenticate : ConnectStatus.UntrustedCertificate;
                }
                else
                {
                    result.Status = ConnectStatus.DriveNotAvailable; // AuthenticateRequestResult.SignInState.Error;
                }
                result.ErrorCode = ex.HResult;
                result.ErrorMessage = ex.Message;
                ConditionalFileLogger.Log($"FTPStorageDevice.Initialize - FAIL-4, ErrorCode: {ex.HResult}, {ex.Message}");
            }
            return result;
        }

2. AutoConnect does not work This is the log data:

2022.08.22 10:02:38.516 5:FTPStorageDevice.Initialize - Extract trusted certificates list
2022.08.22 10:02:38.563 5:FTPStorageDevice.Initialize - File contains 6 entries
2022.08.22 10:02:38.599 5:--FluentFTP-- >         AutoConnect()
2022.08.22 10:02:38.630 5:--FluentFTP-- >         AutoDetect(True, False)
2022.08.22 10:02:38.676 5:--FluentFTP-- >         Connect()
2022.08.22 10:02:38.767 5:--FluentFTP-- Status:   Connecting to ***:21
2022.08.22 10:02:40.866 5:--FluentFTP-- Status:   Disposing FtpSocketStream...
2022.08.22 10:02:40.916 5:--FluentFTP-- >         Connect()
2022.08.22 10:02:41.014 5:--FluentFTP-- Status:   Connecting to ***:990
2022.08.22 10:02:41.105 5:--FluentFTP-- Command:  QUIT
2022.08.22 10:02:41.643 5:--FluentFTP-- Warning:  FtpClient.Disconnect(): Exception caught and discarded while closing control connection: System.InvalidOperationException: This operation is only allowed using a successfully authenticated context.
   at System.Net.Security.SslStream.ThrowNotAuthenticated()
   at System.Net.Security.SslStream.Write(Byte[] buffer, Int32 offset, Int32 count)
   at FluentFTP.FtpSocketStream.Write(Byte[] buffer, Int32 offset, Int32 count)
   at FluentFTP.FtpSocketStream.WriteLine(Encoding encoding, String buf)
   at FluentFTP.FtpClient.Execute(String command)
   at FluentFTP.FtpClient.Disconnect()
2022.08.22 10:02:41.675 5:--FluentFTP-- Status:   Disposing FtpSocketStream...
2022.08.22 10:02:41.715 5:FTPStorageDevice.Initialize - NOT CONNECTED  

It looks like immediately after connecting with port 990 something goes wrong. The certicate validation is also not called but I assume that this is because there is no AUTH command yet (I don't see it in the log).

The code I used to test AutoConnect is:

public StorageDeviceOpenResult Open()
        {
            _certTrusted = true;
            ConditionalFileLogger.Log($"FTPStorageDevice.Open - Host: {Name}, port: {Port}, account: {AccountName}, connectionMode: {FTPConnectionMode}, tlsProtocol: {SslProtocol.ToString()}");

            var result = new StorageDeviceOpenResult() { Status = ConnectStatus.DriveNotAvailable };
            if (string.IsNullOrEmpty(AccountName) ||
                string.IsNullOrEmpty(Password))
            {
                result.Status = ConnectStatus.CouldNotAuthenticate;
                return result;
            }

            try
            {
                // Setup FtpClient
                NetworkCredential credential = new NetworkCredential(AccountName, Password);
                FtpTrace.EnableTracing = false;
                FtpTrace.LogToConsole = false;
                _ftpClient = new FtpClient(Host, Port, credential);
                if (ConditionalFileLogger.Level == ConditionalFileLogger.LoggingLevel.Detail)
                {
                    FtpTrace.EnableTracing = true;
                    _ftpClient.OnLogEvent = OnFTPLogEvent;
                }
                var offset = DateTimeOffset.Now.Offset;
                _ftpClient.TimeZone = 2; // Time zone of server should (with LIST) and must (with MLSD) be in UTC
                _ftpClient.TimeConversion = FtpDate.ServerTime;
                if (FTPConnectionMode != 2)
                {
                    // 1. Prepare certificate handling
                    _certValidationPending = true;
                    _ftpClient.ValidateAnyCertificate = true;
                    _ftpClient.ValidateCertificate += _ftpClient_ValidateCertificate;

                    // 2. Get all trusted certificates in _trustedCerts (will be used in _ftpClient_ValidateCertificate)
                    var folder = Windows.Storage.ApplicationData.Current.LocalFolder;
                    string filePath = $"{folder.Path}\\trusted_certs.json";
                    if (File.Exists(filePath))
                    {
                        ConditionalFileLogger.Log("FTPStorageDevice.Initialize - Extract trusted certificates list");
                        DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(List<TrustedCert>));
                        using (FileStream fs = File.OpenRead(filePath))
                        {
                            _trustedCerts = (List<TrustedCert>)serializer.ReadObject(fs);
                            fs.Close();
                        }
                        ConditionalFileLogger.Log($"FTPStorageDevice.Initialize - File contains {_trustedCerts.Count} entries");
                    }
                    else
                    {
                        ConditionalFileLogger.Log("FTPStorageDevice.Initialize - There is no trusted certificates list");
                    }
                }
                // Connect to the server
                _ftpClient.AutoConnect();
                _certValidationPending = false;
                if (_ftpClient.IsConnected)
                {
                    result.Status = ConnectStatus.Connected;
                    SelectedSslProtocol = SelectedSslProcolToTlsProtocol(_ftpClient.SslProtocolActive);
                    FtpRootDirectory = _ftpClient.GetWorkingDirectory();
                    ConditionalFileLogger.Log("FTPStorageDevice.Initialize - SUCCESS");
                }
                else
                {
                    ConditionalFileLogger.Log("FTPStorageDevice.Initialize - NOT CONNECTED");
                }
            }
            catch (FtpSecurityNotAvailableException e1)
            {
                result.Status = ConnectStatus.TLSNotSupported;
                result.ErrorCode = e1.HResult;
                result.ErrorMessage = e1.Message;
                ConditionalFileLogger.Log($"FTPStorageDevice.Initialize - FAIL-1, ErrorCode: {e1.HResult}, {e1.Message}");
            }
            catch (FtpAuthenticationException e2)
            {
                result.Status = e2.CompletionCode.Equals("421") ? ConnectStatus.DriveNotAvailable : ConnectStatus.AccountNotRegisteredAnymore; // AuthenticateRequestResult.SignInState.ServiceNotAvailable : AuthenticateRequestResult.SignInState.AccountNotRegistered;
                result.ErrorCode = e2.HResult;
                result.ErrorMessage = e2.Message;
                ConditionalFileLogger.Log($"FTPStorageDevice.Initialize - FAIL-2, ErrorCode: {e2.HResult}, {e2.Message}");
            }
            catch (AuthenticationException e3)
            {
                result.Status = _certTrusted ? ConnectStatus.AccountNotRegisteredAnymore : ConnectStatus.UntrustedCertificate; //  AuthenticateRequestResult.SignInState.AccountNotRegistered : AuthenticateRequestResult.SignInState.CertificateError;
                result.ErrorCode = e3.HResult;
                result.ErrorMessage = e3.Message;
                ConditionalFileLogger.Log($"FTPStorageDevice.Initialize - FAIL-3, ErrorCode: {e3.HResult}, {e3.Message}");
            }
            catch (Exception ex)
            {
                // 0x80131515: tls problem
                // 0x80131500: tls problem or wrong address
                // 0x80004004: wrong address
                if (FTPConnectionMode != 2 &&
                    (ex.HResult & 0xFFFF00) == 0x131500)
                {
                    result.Status = _certTrusted ? ConnectStatus.CouldNotAuthenticate : ConnectStatus.UntrustedCertificate;
                }
                else
                {
                    result.Status = ConnectStatus.DriveNotAvailable; // AuthenticateRequestResult.SignInState.Error;
                }
                result.ErrorCode = ex.HResult;
                result.ErrorMessage = ex.Message;
                ConditionalFileLogger.Log($"FTPStorageDevice.Initialize - FAIL-4, ErrorCode: {ex.HResult}, {ex.Message}");
            }
            return result;
        }

I am OK with 39.4.0-BETA1 since, for my application, I see little use for a AutoConnect. The app already has all the necessary stuff in the UI to configure encryption mode, and force TLS1.2 if the default doesn't work. But I can understand that if this would work across all possible FTP/FTPS servers in the world, any application UI would become much simpler.

JosHuybrighs avatar Aug 22 '22 08:08 JosHuybrighs

Released!

https://www.nuget.org/packages/FluentFTP/39.4.0

robinrodricks avatar Aug 23 '22 08:08 robinrodricks

Possible solution:

Implement https://github.com/whSwitching/TLSHandler

robinrodricks avatar Aug 26 '22 16:08 robinrodricks

This rather general issue would profit from: https://github.com/robinrodricks/FluentFTP/wiki/FTPS-Connection-using-GnuTLS

FanDjango avatar Feb 19 '23 11:02 FanDjango