2012-08-25 14 views
15

Ho recentemente cercato di creare un server/client crittografato SSL in C#.Come identificare il nome del mio server per l'autenticazione del server per client in C#

Ho seguito this tutorial su MSDN, tuttavia, è necessario un certificato da creare per l'utilizzo di server e client utilizzando Makecert.exe così ho trovato un esempio e ha creato il certificato multa:

makecert -sr LocalMachine -ss My -n "CN=Test" -sky exchange -sk 123456 c:/Test.cer

ma ora il problema è avviato il server e aspetta per i clienti, quando il client si connette utilizza il nome della macchina che, per quanto ho potuto capire è il mio IP, in questo caso:

127.0.0.1

, e poi richiede il nome server che deve corrispondere al nome server sul certificato (Test.cer). Ho provato diverse combinazioni (come ad esempio "Test" "LocalMachine", "127.0.0.1", ma non posso sembrare ottenere i clienti dato nome del server per abbinare permettendo così il collegamento L'errore che ottengo è:.

Certificate error: RemoteCertificateNameMismatch, RemoteCertificateChainErrors Exception: the remote certificate is invalid according to the validation procedure

ecco il codice che sto utilizzando differisce dall'esempio MSDN solo nel fatto che assegno il percorso certificato per il server in app e il nome della macchina e il nome del server del cliente troppo:

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.<EOF>"); 
       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 
      // "<EOF>" 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("<EOF>") != -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 "<EOF>". 
      byte[] messsage = Encoding.UTF8.GetBytes("Hello from the client.<EOF>"); 
      // 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 
      // "<EOF>" 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("<EOF>") != -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(); 
     } 
    } 
} 

EDIT:

Il server accetta la connessione client e tutto, ma va in timeout durante l'attesa per il cliente per inviare un messaggio. (Il client solito l'autenticazione con il server a causa del nome del server nel certificato di essere diversa da quella che ho fornito nel client) e questo è il mio pensiero su di essa solo per chiarire

UPDATE:

Secondo una risposta che hanno cambiato il creatore certficiate a:

makecert -sr LocalMachine -ss My -n "CN=localhost" -sky exchange -sk 123456 c:/Test.cer 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); 

ora ottengo l'eccezione:

RemoteCertificateChainErrors Exception: the remote certificate is invalid according to the validation procedure

che sta accadendo qui:

// 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; 
      } 
+0

Si sta utilizzando un certificato per il cliente? Qual è il valore di 'serverName' nell'ultimo snippet di codice? Inoltre, per favore pubblica il valore di 'sslPolicyErrors' nel metodo di validazione del client. –

risposta

9

La risposta può essere trovata nella sezione SslStream.AuthenticateAsClient Method Note:.

The value specified for targetHost must match the name on the server's certificate.

Se si utilizza per il server di un certificato che è soggetto è "CN = localhost", è necessario chiamare AuthenticateAsClient con "localhost" come parametro targetHost per autenticare con successo sul lato client. Se si desidera utilizzare "CN = David-PC" come oggetto del certificato, è necessario chiamare AuthenticateAsClient con "David-PC" come targetHost. SslStream verifica l'identità del server facendo corrispondere il nome del server che si desidera connettere (e che si passa a AuthenticateAsClient) con l'oggetto nel certificato ricevuto dal server. La pratica è che il nome della macchina che esegue il server corrisponde al nome dell'oggetto del certificato e nel client si passa lo stesso nome host a AuthenticateAsClient come si è usato per aprire la connessione (con TcpClient in questo caso).

Tuttavia ci sono altre condizioni per stabilire con successo una connessione SSL tra server e client: il certificato passato a AuthenticateAsServer deve avere una chiave privata, deve essere attendibile sul computer client e non deve avere alcuna restrizione sull'uso della chiave relativa all'utilizzo per stabilire sessioni SSL.

Ora correlato all'esempio del codice, il problema è legato alla generazione e all'utilizzo del certificato.

  • Non sta fornendo un emittente per il certificato e in questo modo non ci si può fidare - Questa è la causa della RemoteCertificateChainErrors eccezione. Suggerisco di creare un certificato autofirmato per scopi di sviluppo specificando l'opzione -r di makecert.

  • Per essere certi, un certificato deve essere autofirmato e inserito in una posizione attendibile nell'archivio certificati di Windows o deve essere collegato a una catena di firme a un'autorità di certificazione già affidabile. Quindi, invece dell'opzione -ss My che posizionerà il certificato nel negozio personale, utilizzare -ss root che lo posizionerà nelle autorità di certificazione radice attendibili e verrà considerato attendibile sulla macchina (dal codice presumo che il client sia in esecuzione sulla stessa macchina con il server e viene generato anche il certificato su di esso).

  • Se si specifica un file di output su makecert, verrà esportato il certificato come .cer ma questo formato contiene solo la chiave pubblica, non la chiave privata necessaria per stabilire una connessione SSL. Il modo più semplice è leggere il certificato dall'archivio certificati di Windows nel codice del server. (Puoi anche esportarlo dal negozio in un altro formato che consente di memorizzare la chiave privata come descritto qui Export a certificate with the private key e leggere quel file nel codice del server).

Potete trovare maggiori dettagli sulle opzioni MakeCert usati qui Certificate Creation Tool (Makecert.exe)

In conclusione il codice deve le seguenti modifiche a correre (testato con i tuoi ultimi aggiornamenti del codice):

  • Utilizza il seguente comando per generare il certificato:

makecert -sr LocalMachine -ss root -r -n "CN=localhost" -sky exchange -sk 123456

  • Leggere il certificato da Windows Store certificato invece di un file (per la semplicità di questo esempio), consentendo di sostituire

serverCertificate = X509Certificate.CreateFromCertFile(certificate);

nel codice server con:

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]; 
} 

Ricordatevi di sostituire "CN = localhost" con l'oggetto del certificato che si intende utilizzare se si modifica il codice in un secondo momento (in questa situazione dovrebbe essere lo stesso valore dell'opzione -n ​​passata a makecert). Considerare inoltre di utilizzare il nome della macchina che esegue il server anziché localhost nel soggetto del certificato del server.

+1

+1 Grazie mille questo ha risolto il problema :) –

+0

Felice di essere stato di aiuto :) –

5

CN di un certificato del server deve essere esattamente lo stesso nome di dominio del server. Suppongo che nel tuo caso il nome comune debba essere "localhost" (senza virgolette).

Importante: per sicurezza, come si potrebbe aver letto in altre risposte, non utilizzare mai CN="localhost" in produzione.

+0

@DavidKroukamp, ​​potresti non aver visto il mio ultimo commento. Puoi dare le risposte? –

1

Per fare in modo che funzioni con WCF è necessario prima creare un certificato dell'autorità di root autofirmato e quindi utilizzarlo per creare il certificato per localhost.

Sto pensando che lo stesso potrebbe valere per il tuo progetto, per favore dai un'occhiata a questo articolo How to: Create Temporary Certificates for Use During Development per i dettagli.

1

Hai provato :?

Creare il certificato per un nome di dominio completo come example.net (è bene usare example.net, example.com o example.org per tutto ciò che non è deliberatamente un vero nome) o il nome che verrà utilizzato in uso dal vivo, se questo è un singolo sito e si sapere cosa sarà

Aggiorna il file hosts in modo che utilizzi 127.0.0.1 per tale nome.

4

Innanzitutto, non creare un certificato con l'oggetto "CN = localhost" o equivalente. Non verrà mai utilizzato in produzione, quindi non farlo. Distribuiscilo sempre al tuo hostname, ad es. CN = "mycomputer" e utilizzare il nome host quando ci si connette piuttosto che localhost. Puoi specificare più nomi usando l'estensione "nome alternativo del soggetto" ma non sembra che lo makecert lo supporti.

In secondo luogo, quando si rilascia un certificato SSL del server, è necessario aggiungere l'OID "autenticazione server" all'estensione EKU (Enhanced Key usage) del certificato. Aggiungi il parametro -eku 1.3.6.1.5.5.7.3.1 a makecert nell'esempio. Se si desidera eseguire l'autenticazione del certificato client, utilizzare l'OID "client authentication" di 1.3.6.1.5.5.7.3.2.

Infine, il certificato predefinito creato da makecert utilizza MD5 come algoritmo di hash. MD5 è considerato insicuro e, sebbene non influenzi i test, prendi l'abitudine di usare SHA1. Aggiungi -a sha1 ai parametri makecert sopra per forzare SHA1. La dimensione della chiave di default dovrebbe anche essere aumentata da 1024 bit a 2048 bit, ma si ottiene l'idea.

+0

Per quanto ne so, anche sha1 non è così sicuro al giorno d'oggi ...meglio provare con -a sha256 Oltre a ciò, è importante sottolineare che anche la lunghezza della chiave è importante, dal momento che alcuni browser (chrome?) hanno iniziato a lamentarsi di "chiavi deboli" -> che è AFAIK breve e/o utilizzabile algoritmi di hashing interrotti – Luke

+1

@Luke sei corretto ma le versioni precedenti di Windows (XP e 2003) non supportano i certificati utilizzando SHA256 (o meglio). Se questo è un problema dipende dal cliente. – akton

+0

Giusto ... c'è un bel casino laggiù! Per quanto mi ricordo, alcuni mesi fa probabilmente ho trovato un modo per "far sapere al sistema" come supportare gli algoritmi di hashing più recenti su quei sistemi, ma era un modo abbastanza imbarazzante per farlo ... e un incubo da implementare .. Inoltre ci sono diverse versioni del file makecert.exe, e quelle precedenti non accettavano affatto il parametro sha256. Ho dovuto scoprire quelli più recenti tra le varie cartelle di VS, SDK e di sistema sul mio PC di sviluppo ... – Luke

Problemi correlati