// Copyright (c) 2004, 2021, Oracle and/or its affiliates. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License, version 2.0, as // published by the Free Software Foundation. // // This program is also distributed with certain software (including // but not limited to OpenSSL) that is licensed under separate terms, // as designated in a particular file or component or in included license // documentation. The authors of MySQL hereby grant you an // additional permission to link the program and your derivative works // with the separately licensed software that they have included with // MySQL. // // Without limiting anything contained in the foregoing, this file, // which is part of MySQL Connector/NET, is also subject to the // Universal FOSS Exception, version 1.0, a copy of which can be found at // http://oss.oracle.com/licenses/universal-foss-exception. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // See the GNU General Public License, version 2.0, for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA using MySql.Data.common; using MySql.Data.MySqlClient; using System; using System.Collections.Generic; using System.IO; using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using Sog.Properties; namespace MySql.Data.Common { /// /// Handles SSL connections for the Classic and X protocols. /// internal class Ssl { #region Fields /// /// Contains the connection options provided by the user. /// private MySqlConnectionStringBuilder _settings; /// /// A flag to establish how certificates are to be treated and validated. /// private bool _treatCertificatesAsPemFormat; /// /// Defines the supported TLS protocols. /// private static SslProtocols[] tlsProtocols = new SslProtocols[] { SslProtocols.Tls12, SslProtocols.Tls11, SslProtocols.Tls }; private static List deprecatedTlsProtocols = new List { SslProtocols.Tls11, SslProtocols.Tls }; private static Dictionary tlsConnectionRef = new Dictionary(); private static Dictionary tlsRetry = new Dictionary(); private static Object thisLock = new Object(); #endregion public Ssl(MySqlConnectionStringBuilder settings) { this._settings = settings; // Set default value to true since PEM files is the standard for MySQL SSL certificates. this._treatCertificatesAsPemFormat = true; } public Ssl(string server, MySqlSslMode sslMode, string certificateFile, MySqlCertificateStoreLocation certificateStoreLocation, string certificatePassword, string certificateThumbprint, string sslCa, string sslCert, string sslKey, string tlsVersion) { this._settings = new MySqlConnectionStringBuilder() { Server = server, SslMode = sslMode, CertificateFile = certificateFile, CertificateStoreLocation = certificateStoreLocation, CertificatePassword = certificatePassword, CertificateThumbprint = certificateThumbprint, SslCa = sslCa, SslCert = sslCert, SslKey = sslKey, TlsVersion = tlsVersion }; // Set default value to true since PEM files is the standard for MySQL SSL certificates. this._treatCertificatesAsPemFormat = true; } /// /// Retrieves a collection containing the client SSL PFX certificates. /// /// Dependent on connection string settings. /// Either file or store based certificates are used. private X509CertificateCollection GetPFXClientCertificates() { X509CertificateCollection certs = new X509CertificateCollection(); // Check for file-based certificate if (this._settings.CertificateFile != null) { X509Certificate2 clientCert = new X509Certificate2( this._settings.CertificateFile, this._settings.CertificatePassword); certs.Add(clientCert); return certs; } if (this._settings.CertificateStoreLocation == MySqlCertificateStoreLocation.None) { return certs; } StoreLocation location = (this._settings.CertificateStoreLocation == MySqlCertificateStoreLocation.CurrentUser) ? StoreLocation.CurrentUser : StoreLocation.LocalMachine; try { // Check for store-based certificate X509Store store = new X509Store(StoreName.My, location); store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly); if (this._settings.CertificateThumbprint == null) { // Return all certificates from the store. certs.AddRange(store.Certificates); if (certs.Count == 0) { throw new MySqlException("No certificates were found in the certificate store"); } return certs; } else { bool validateCert = this._settings.SslMode == MySqlSslMode.VerifyCA || this._settings.SslMode == MySqlSslMode.VerifyFull; // Find certificate with given thumbprint certs.AddRange(store.Certificates.Find( X509FindType.FindByThumbprint, this._settings.CertificateThumbprint, validateCert)); if (certs.Count == 0) { throw new MySqlException(String.Format(Resources.InvalidCertificateThumbprint, this._settings.CertificateThumbprint)); } return certs; } } catch (CryptographicException ex) { throw new MySqlException("Certificate couldn't be loaded from the CertificateStoreLocation", ex); } } /// /// Initiates the SSL connection. /// /// The base stream. /// The encoding used in the SSL connection. /// The connection string used to establish the connection. /// A instance ready to initiate an SSL connection. public MySqlStream StartSSL(ref Stream baseStream, Encoding encoding, string connectionString) { // If SslCa connection option was provided, check for the file extension as it can also be set as a PFX file. if (this._settings.SslCa != null) { var fileExtension = this.GetCertificateFileExtension(this._settings.SslCa, true); if (fileExtension != null) { this._treatCertificatesAsPemFormat = fileExtension != "pfx"; } } RemoteCertificateValidationCallback sslValidateCallback = new RemoteCertificateValidationCallback(this.ServerCheckValidation); SslStream ss = new SslStream(baseStream, false, sslValidateCallback, null); X509CertificateCollection certs = (this._treatCertificatesAsPemFormat && this._settings.CertificateStoreLocation == MySqlCertificateStoreLocation.None) ? new X509CertificateCollection() : this.GetPFXClientCertificates(); string connectionId = connectionString.GetHashCode().ToString(); SslProtocols tlsProtocol = SslProtocols.None; if (this._settings.TlsVersion != null) { #if NET452 || NETSTANDARD2_0 if (_settings.TlsVersion.Equals("Tls13", StringComparison.OrdinalIgnoreCase)) throw new NotSupportedException(Resources.Tlsv13NotSupported); #endif SslProtocols sslProtocolsToUse = (SslProtocols)Enum.Parse(typeof(SslProtocols), this._settings.TlsVersion); List listProtocols = new List(); #if NET48 || NETSTANDARD2_1 || NET5_0 if (sslProtocolsToUse.HasFlag((SslProtocols)12288)) listProtocols.Add((SslProtocols)12288); #endif if (sslProtocolsToUse.HasFlag(SslProtocols.Tls12)) listProtocols.Add(SslProtocols.Tls12); if (sslProtocolsToUse.HasFlag(SslProtocols.Tls11)) { listProtocols.Add(SslProtocols.Tls11); } if (sslProtocolsToUse.HasFlag(SslProtocols.Tls)) { listProtocols.Add(SslProtocols.Tls); } tlsProtocols = listProtocols.ToArray(); } lock (thisLock) { if (tlsConnectionRef.ContainsKey(connectionId)) { tlsProtocol = tlsConnectionRef[connectionId]; } else { if (!tlsRetry.ContainsKey(connectionId)) { tlsRetry[connectionId] = 0; } for (int i = tlsRetry[connectionId]; i < tlsProtocols.Length; i++) { tlsProtocol |= tlsProtocols[i]; } } try { tlsProtocol = (tlsProtocol == SslProtocols.None) ? SslProtocols.Tls : tlsProtocol; if (!ss.AuthenticateAsClientAsync(this._settings.Server, certs, tlsProtocol, false).Wait((int)this._settings.ConnectionTimeout * 1000)) { throw new AuthenticationException($"Authentication to host '{this._settings.Server}' failed."); } tlsConnectionRef[connectionId] = tlsProtocol; tlsRetry.Remove(connectionId); } catch (AggregateException ex) { if (ex.GetBaseException() is IOException) { tlsConnectionRef.Remove(connectionId); if (tlsRetry.ContainsKey(connectionId)) { if (tlsRetry[connectionId] > tlsProtocols.Length) { throw new MySqlException(Resources.SslConnectionError, ex); } tlsRetry[connectionId] += 1; } } throw ex.GetBaseException(); } } baseStream = ss; if (deprecatedTlsProtocols.Contains(ss.SslProtocol)) { MySqlTrace.LogWarning(-1, string.Format(Resources.TlsDeprecationWarning, ss.SslProtocol)); } MySqlStream stream = new MySqlStream(ss, encoding, false); stream.SequenceByte = 2; return stream; } /// /// Verifies the SSL certificates used for authentication. /// /// An object that contains state information for this validation. /// The MySQL server certificate used to authenticate the remote party. /// The chain of certificate authorities associated with the remote certificate. /// One or more errors associated with the remote certificate. /// true if no errors were found based on the selected SSL mode; false, otherwise. private bool ServerCheckValidation(object sender, System.Security.Cryptography.X509Certificates.X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { if (sslPolicyErrors == SslPolicyErrors.None) { return true; } if (this._settings.SslMode == MySqlSslMode.Required || this._settings.SslMode == MySqlSslMode.Preferred) { // Tolerate all certificate errors. return true; } // Validate PEM certificates using Bouncy Castle. if (this._treatCertificatesAsPemFormat) { SslPemCertificateValidator.ValidateCertificate(certificate, this._settings); return true; } // Validate PFX certificate errors. else if (this._settings.SslMode == MySqlSslMode.VerifyCA && sslPolicyErrors == SslPolicyErrors.RemoteCertificateNameMismatch) { // Tolerate name mismatch in certificate, if full validation is not requested. return true; } return false; } /// /// Gets the extension of the specified file. /// /// The path of the file. /// Flag to indicate if the result should be converted to lower case. /// The . character is ommited from the result. /// private string GetCertificateFileExtension(string filePath, bool toLowerCase) { if (filePath == null || !File.Exists(filePath)) { return null; } var extension = Path.GetExtension(filePath); extension = string.IsNullOrEmpty(extension) ? null : extension.Substring(extension.IndexOf(".") + 1); return toLowerCase ? extension.ToLowerInvariant() : extension; } } }