From: 011netservice@gmail.com Date: 2022-04-24 Subject: 20211127-ServerAuthenticationByClientC#.txt --------- 2020-11-27 How do I identify my server name for server authentication by client in c# https://stackoverflow.com/questions/12120630/how-do-i-identify-my-server-name-for-server-authentication-by-client-in-c-sharp I have recently been trying to make a SSL encrypted Server/Client in C#. I have followed this tutorial on MSDN, however, it required a certificate to be created for the server and client usage using makecert.exe so I found an example and it created the certificate fine: 已建立以下 Test.cer 檔案: makecert -sr LocalMachine -ss My -n "CN=Test" -sky exchange -sk 123456 c:/Test.cer 但是無法連線到 127.0.0.1, but now the problem is the server starts and waits for clients, when the client connects it uses the machine name which as far as I can gather is my IP in this case: 127.0.0.1 測試過其他的伺服器名稱也不成功, , and then it requires the servers name which must match the servers name on the certificate (Test.cer). I have tried multiple combinations (such as "Test" "LocalMachine","127.0.0.1" but cant seem to get the clients given server name to match thus allowing the connection. The error I get is: 錯誤訊息為: Certificate error: RemoteCertificateNameMismatch, RemoteCertificateChainErrors Exception: the remote certificate is invalid according to the validation procedure 測試的程式碼如下, 擷取自 MSDN, 只改掉 path, machine name 以及 server name. here is the code I'm using it differs from the MSDN example only in the fact that I assign the certificate path for the server in the app and the machine name and server name of the client too: SslTcpServer.cs using System; using System.Collections; using System.Net; using System.Net.Sockets; using System.Net.Security; using System.Security.Authentication; using System.Text; using System.Security.Cryptography.X509Certificates; using System.IO; namespace Examples.System.Net { public sealed class SslTcpServer { static X509Certificate serverCertificate = null; // The certificate parameter specifies the name of the file // containing the machine certificate. public static void RunServer(string certificate) { serverCertificate = X509Certificate.CreateFromCertFile(certificate); // Create a TCP/IP (IPv4) socket and listen for incoming connections. TcpListener listener = new TcpListener(IPAddress.Any, 8080); listener.Start(); while (true) { Console.WriteLine("Waiting for a client to connect..."); // Application blocks while waiting for an incoming connection. // Type CNTL-C to terminate the server. TcpClient client = listener.AcceptTcpClient(); ProcessClient(client); } } static void ProcessClient(TcpClient client) { // A client has connected. Create the // SslStream using the client's network stream. SslStream sslStream = new SslStream( client.GetStream(), false); // Authenticate the server but don't require the client to authenticate. try { sslStream.AuthenticateAsServer(serverCertificate, false, SslProtocols.Tls, true); // Display the properties and settings for the authenticated stream. DisplaySecurityLevel(sslStream); DisplaySecurityServices(sslStream); DisplayCertificateInformation(sslStream); DisplayStreamProperties(sslStream); // Set timeouts for the read and write to 5 seconds. sslStream.ReadTimeout = 5000; sslStream.WriteTimeout = 5000; // Read a message from the client. Console.WriteLine("Waiting for client message..."); string messageData = ReadMessage(sslStream); Console.WriteLine("Received: {0}", messageData); // Write a message to the client. byte[] message = Encoding.UTF8.GetBytes("Hello from the server."); Console.WriteLine("Sending hello message."); sslStream.Write(message); } catch (AuthenticationException e) { Console.WriteLine("Exception: {0}", e.Message); if (e.InnerException != null) { Console.WriteLine("Inner exception: {0}", e.InnerException.Message); } Console.WriteLine("Authentication failed - closing the connection."); sslStream.Close(); client.Close(); return; } finally { // The client stream will be closed with the sslStream // because we specified this behavior when creating // the sslStream. sslStream.Close(); client.Close(); } } static string ReadMessage(SslStream sslStream) { // Read the message sent by the client. // The client signals the end of the message using the // "" marker. byte[] buffer = new byte[2048]; StringBuilder messageData = new StringBuilder(); int bytes = -1; do { // Read the client's test message. bytes = sslStream.Read(buffer, 0, buffer.Length); // Use Decoder class to convert from bytes to UTF8 // in case a character spans two buffers. Decoder decoder = Encoding.UTF8.GetDecoder(); char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)]; decoder.GetChars(buffer, 0, bytes, chars, 0); messageData.Append(chars); // Check for EOF or an empty message. if (messageData.ToString().IndexOf("") != -1) { break; } } while (bytes != 0); return messageData.ToString(); } static void DisplaySecurityLevel(SslStream stream) { Console.WriteLine("Cipher: {0} strength {1}", stream.CipherAlgorithm, stream.CipherStrength); Console.WriteLine("Hash: {0} strength {1}", stream.HashAlgorithm, stream.HashStrength); Console.WriteLine("Key exchange: {0} strength {1}", stream.KeyExchangeAlgorithm, stream.KeyExchangeStrength); Console.WriteLine("Protocol: {0}", stream.SslProtocol); } static void DisplaySecurityServices(SslStream stream) { Console.WriteLine("Is authenticated: {0} as server? {1}", stream.IsAuthenticated, stream.IsServer); Console.WriteLine("IsSigned: {0}", stream.IsSigned); Console.WriteLine("Is Encrypted: {0}", stream.IsEncrypted); } static void DisplayStreamProperties(SslStream stream) { Console.WriteLine("Can read: {0}, write {1}", stream.CanRead, stream.CanWrite); Console.WriteLine("Can timeout: {0}", stream.CanTimeout); } static void DisplayCertificateInformation(SslStream stream) { Console.WriteLine("Certificate revocation list checked: {0}", stream.CheckCertRevocationStatus); X509Certificate localCertificate = stream.LocalCertificate; if (stream.LocalCertificate != null) { Console.WriteLine("Local cert was issued to {0} and is valid from {1} until {2}.", localCertificate.Subject, localCertificate.GetEffectiveDateString(), localCertificate.GetExpirationDateString()); } else { Console.WriteLine("Local certificate is null."); } // Display the properties of the client's certificate. X509Certificate remoteCertificate = stream.RemoteCertificate; if (stream.RemoteCertificate != null) { Console.WriteLine("Remote cert was issued to {0} and is valid from {1} until {2}.", remoteCertificate.Subject, remoteCertificate.GetEffectiveDateString(), remoteCertificate.GetExpirationDateString()); } else { Console.WriteLine("Remote certificate is null."); } } public static void Main(string[] args) { string certificate = "c:/Test.cer"; SslTcpServer.RunServer(certificate); } } } SslTcpClient.cs using System; using System.Collections; using System.Net; using System.Net.Security; using System.Net.Sockets; using System.Security.Authentication; using System.Text; using System.Security.Cryptography.X509Certificates; using System.IO; namespace Examples.System.Net { public class SslTcpClient { private static Hashtable certificateErrors = new Hashtable(); // The following method is invoked by the RemoteCertificateValidationDelegate. public static bool ValidateServerCertificate( object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { if (sslPolicyErrors == SslPolicyErrors.None) return true; Console.WriteLine("Certificate error: {0}", sslPolicyErrors); // Do not allow this client to communicate with unauthenticated servers. return false; } public static void RunClient(string machineName, string serverName) { // Create a TCP/IP client socket. // machineName is the host running the server application. TcpClient client = new TcpClient(machineName, 8080); Console.WriteLine("Client connected."); // Create an SSL stream that will close the client's stream. SslStream sslStream = new SslStream( client.GetStream(), false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null ); // The server name must match the name on the server certificate. try { sslStream.AuthenticateAsClient(serverName); } catch (AuthenticationException e) { Console.WriteLine("Exception: {0}", e.Message); if (e.InnerException != null) { Console.WriteLine("Inner exception: {0}", e.InnerException.Message); } Console.WriteLine("Authentication failed - closing the connection."); client.Close(); return; } // Encode a test message into a byte array. // Signal the end of the message using the "". byte[] messsage = Encoding.UTF8.GetBytes("Hello from the client."); // Send hello message to the server. sslStream.Write(messsage); sslStream.Flush(); // Read message from the server. string serverMessage = ReadMessage(sslStream); Console.WriteLine("Server says: {0}", serverMessage); // Close the client connection. client.Close(); Console.WriteLine("Client closed."); } static string ReadMessage(SslStream sslStream) { // Read the message sent by the server. // The end of the message is signaled using the // "" marker. byte[] buffer = new byte[2048]; StringBuilder messageData = new StringBuilder(); int bytes = -1; do { bytes = sslStream.Read(buffer, 0, buffer.Length); // Use Decoder class to convert from bytes to UTF8 // in case a character spans two buffers. Decoder decoder = Encoding.UTF8.GetDecoder(); char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)]; decoder.GetChars(buffer, 0, bytes, chars, 0); messageData.Append(chars); // Check for EOF. if (messageData.ToString().IndexOf("") != -1) { break; } } while (bytes != 0); return messageData.ToString(); } public static void Main(string[] args) { string serverCertificateName = null; string machineName = null; /* // User can specify the machine name and server name. // Server name must match the name on the server's certificate. machineName = args[0]; if (args.Length < 2) { serverCertificateName = machineName; } else { serverCertificateName = args[1]; }*/ machineName = "127.0.0.1"; serverCertificateName = "David-PC";// tried Test, LocalMachine and 127.0.0.1 SslTcpClient.RunClient(machineName, serverCertificateName); Console.ReadKey(); } } } Server 可成功連線, 但是可能因為 憑證的server name 跟連線的 server name 不同, 導致錯誤. EDIT: The server accepts the clients connection and everything but it times out while waiting for the client to send a message. (The client wont authenticate with server due to the server name in the certificate being different from the one I supplied in the client) well thats my thoughts on it just to clarify 改成 "LocalMachine" 以後, UPDATE: According to an answer I have changed the certficiate maker to: makecert -sr LocalMachine -ss My -n "CN=localhost" -sky exchange -sk 123456 c:/Test.cer 並且改成如下程式碼, (測試過 Test, LocalMachine and 127.0.0.1 and in my client I have: machineName = "127.0.0.1"; serverCertificateName = "localhost";// tried Test, LocalMachine and 127.0.0.1 SslTcpClient.RunClient(machineName, serverCertificateName); 新錯誤如下: now I get the exception: RemoteCertificateChainErrors Exception: the remote certificate is invalid according to the validation procedure 錯誤點為: sslStream.AuthenticateAsClient(serverName); which is occuring here: // The server name must match the name on the server certificate. try { sslStream.AuthenticateAsClient(serverName); } catch (AuthenticationException e) { Console.WriteLine("Exception: {0}", e.Message); if (e.InnerException != null) { Console.WriteLine("Inner exception: {0}", e.InnerException.Message); } Console.WriteLine("Authentication failed - closing the connection. "+ e.Message); client.Close(); return; } 答案在 The answer can be found at SslStream.AuthenticateAsClient Method Remarks section: The value specified for targetHost must match the name on the server's certificate. 憑證建立為 "CN=localhost"時, 呼叫 AuthenticateAsClient() 就必須傳入參數 targetHost=localhost. If you use for the server a certificate whose subject is "CN=localhost", you must call AuthenticateAsClient with "localhost" as targetHost parameter to successfully authenticate it on the client side. If you would use "CN=David-PC" as certificate subject than you must call AuthenticateAsClient with "David-PC" as targetHost. SslStream 連線會檢查 server name, 以及呼叫 AuthenticateAsClient() 傳入參數, 以及憑證中的 "CN" 參數三者必須相同. SslStream checks the server identity by matching the server name that you intend to connect (and which you pass to AuthenticateAsClient) with the subject in the certificate received from the server. The practice is that the machine name that runs the server matches the name of the certificate's subject, and in the client you pass the same hostname to AuthenticateAsClient as you have used for opening the connection (with TcpClient in this case). 此外, 呼叫 AuthenticateAsServer()必須傳入包含 private key 的憑證, 才能讓 client 建立可信賴的ssl 連線. However there are other conditions to successfully establish SSL connection between servers and clients: the certificate passed to AuthenticateAsServer must have a private key, it must be trusted on the client machine and must not have any key usage restrictions related to usage for establishing SSL sessions. Now related to your code sample, your problem is related to the generation and usage of the certificate. 1. You are not providing an issuer for your certificate and in this way it can't be trusted - this is the cause of the RemoteCertificateChainErrors Exception. I suggest to create a self signed certificate for development purposes specifying the -r option of makecert. 造成 RemoteCertificateChainErrors 的錯誤原因為, 若憑證內容沒有發行者是無法被信賴的. 因此可以 -r 選項 建立開發專用的自我簽核憑證. 2. To be trusted a certificate must either be self-signed and placed in a trusted location in the Windows Certificate Store or must be linked with a chain of signatures to an already trusted Certificate Authority. So instead of the -ss My option which will place the certificate in the Personal store use -ss root that will place it in the Trusted Root Certification Authorities and it will be trusted on your machine (from the code I assume that your client is running on the same machine with the server and also the certificate is generated on it). 可被信賴的憑證除了必須為 自我簽核憑證 以外, 還必須(被放在可信賴的 Windows Certificate Store 區域中), 或是 (可連結到已經可信賴的憑證) 所以 可以 --ss root 取代 --ss My 將 自我簽核憑證 存放在 Windows Certificate Store 區域中 3. If you specify a output file to makecert it will export the certificate as .cer but this format contains only the public key, not the private key that is needed by the server to establish a SSL connection. The easiest way is to read the certificate from the Windows Certificate store in the server code. (You can also export it from the store in another format that allows storing the private key as described here Export a certificate with the private key and read that file in the server code). 若把憑證匯出為 .cer檔案, 則只有 public key, 沒有 private key, 無法使用於 SSL 連線. You can find details about the makecert options used here Certificate Creation Tool (Makecert.exe) 結論: In conclusion your code needs the following changes to run (tested with your latest code updates): 1. 改用以下指令建立(self signed certificate) Use the following command to generate the certificate: makecert -sr LocalMachine -ss root -r -n "CN=localhost" -sky exchange -sk 123456 -r 選項 建立開發專用的自我簽核憑證. 替代掉 makecert -sr LocalMachine -ss My -n "CN=localhost" -sky exchange -sk 123456 c:/Test.cer 2. 改從 (Windows Certificate Store) 讀取憑證檔案 Read the certificate from Windows Certificate Store instead of a file (for the simplicity of this example), so replace serverCertificate = X509Certificate.CreateFromCertFile(certificate); server 端的程式碼改成從 (Windows Certificate Store) 讀取憑證檔案, 並且 "CN=localhost" 跟 makecert -n "CN=localhost" 必須一致. in the server code with: X509Store store = new X509Store(StoreName.Root, StoreLocation.LocalMachine); store.Open(OpenFlags.ReadOnly); var certificates = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, "CN=localhost", false); store.Close(); if (certificates.Count == 0) { Console.WriteLine("Server certificate not found..."); return; } else { serverCertificate = certificates[0]; } Please remember to replace "CN=localhost" with the subject of the certificate that you intend to use if you change the code later (in this situation should be the same value as the -n option passed to makecert). Also consider to use the machine name that runs the server instead of localhost in the server certificate's subject. 此外, 執行時, (CN 名稱) 跟 (server's domain name) 也必須一致! 例如: localhost. A server certificate's CN must be exactly the same as the server's domain name. I suppose, in your case the common name must be "localhost" (w/o quotes). Important: for sure, as you might have read in other answers, never use CN="localhost" in production. First, do not create a certificate with the subject "CN=localhost" or equivalent. It is never going to be used in production so don't do it. Always issue it to your computer's hostname, e.g. CN="mycomputer", and use the host name when connecting to it rather than localhost. You can specify multiple names using the "subject alternate name" extension but makecert does not appear to support it. Second, when issuing a server SSL certificate, you need to add the "server authentication" OID to the enhanced key usage (EKU) extension of the certificate. Add the -eku 1.3.6.1.5.5.7.3.1 parameter to makecert in your example. If you want to do client certificate authentication, use the "client authentication" OID of 1.3.6.1.5.5.7.3.2. Lastly, the default certificate created by makecert uses MD5 as its hashing algorithm. MD5 is considered insecure and, although it will not affect your testing, get into the habit of using SHA1. Add -a sha1 to the makecert parameters above to force SHA1. The default key size should also be increased from 1024-bits to 2048-bits but you get the idea. Have you tried:? Create the certificate for a full domain name like example.net (it's good to use example.net, example.com or example.org for anything that's deliberately not a real name) or the name that will be used in live use if that's a single site and you know what it will be. Update your hosts file so that it will use 127.0.0.1 for that name. To get this to work with WCF it is neccessary to first create a self-signed root authority certificate and then use it to create the certificate for localhost. I'm thinking the same might apply to your project as well, please have a look at this article How to: Create Temporary Certificates for Use During Development for details. How to: Create Temporary Certificates for Use During Development: https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/how-to-create-temporary-certificates-for-use-during-development?redirectedfrom=MSDN