Как определить имя моего сервера для проверки подлинности сервера клиентом в С#
Недавно я пытался создать SSL-шифрованный сервер/клиент в С#.
Я следил за этим учебным пособием по MSDN, однако для создания сервера и клиента он должен был создать сертификат, используя makecert.exe. Я нашел пример и создал сертификат:
makecert -sr LocalMachine -ss My -n "CN = Test" -sky exchange -sk 123456 c:/Test.cer
но теперь проблема заключается в том, что сервер запускается и ждет клиентов, когда клиент подключается к нему, использует имя машины, которое, насколько я могу собрать, это мой IP в этом случае:
127.0.0.1
а затем требуется имя сервера, которое должно соответствовать имени сервера в сертификате (Test.cer). Я попытался использовать несколько комбинаций (например, "Test", "LocalMachine", "127.0.0.1", но, похоже, не позволяет получить указанное имя сервера, что позволяет подключиться. Ошибка, которую я получаю:
Ошибка сертификата: RemoteCertificateNameMismatch, RemoteCertificateChainErrors Исключение: удаленный сертификат недействителен в соответствии с процедурой проверки
вот код, который я использую, он отличается от примера MSDN только тем фактом, что я назначаю путь сертификата для сервера в приложении, а также имя компьютера и имя сервера клиента:
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 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 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 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 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 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:
Сервер принимает соединение с клиентом и все, но он не работает, ожидая, пока клиент отправит сообщение. (Клиент не будет аутентифицироваться с сервером из-за того, что имя сервера в сертификате отличается от того, которое я поставил на клиенте). Хорошо, что мои мысли на нем просто разъясняют
UPDATE:
В соответствии с ответом я изменил создателя certficiate на:
makecert -sr LocalMachine -ss My -n "CN = localhost" -sky exchange -sk 123456 c:/Test.cer и у моего клиента у меня есть:
machineName = "127.0.0.1";
serverCertificateName = "localhost";// tried Test, LocalMachine and 127.0.0.1
SslTcpClient.RunClient(machineName, serverCertificateName);
теперь я получаю исключение:
RemoteCertificateChainErrors Исключение: удаленный сертификат недействителен в соответствии с процедурой проверки
который происходит здесь:
// 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;
}
Ответы
Ответ 1
Ответ можно найти в SslStream.AuthenticateAsClient Method Раздел примечаний:
Значение, указанное для targetHost, должно совпадать с именем в сертификате сервера.
Если для сервера используется сертификат "CN = localhost" , вы должны вызвать AuthenticateAsClient с "localhost" в качестве параметра targetHost для успешной аутентификации на стороне клиента. Если вы будете использовать "CN = David-PC" в качестве объекта сертификата, вы должны вызвать AuthenticateAsClient с "David-PC" в качестве targetHost. SslStream проверяет идентификатор сервера, сопоставляя имя сервера, которое вы собираетесь подключить (и которое вы передаете AuthenticateAsClient), с субъектом в сертификате, полученном с сервера. Практика заключается в том, что имя машины, которая запускает сервер, соответствует имени объекта сертификата, а в клиенте вы передаете одно и то же имя хоста AuthenticateAsClient, как вы использовали для открытия соединения (в этом случае TcpClient).
Однако существуют и другие условия для успешного установления SSL-соединения между серверами и клиентами: сертификат, переданный AuthenticateAsServer, должен иметь закрытый ключ, он должен быть доверен на клиентской машине и не должен иметь никаких ограничений использования ключа, связанных с использованием для установления SSL-сессии.
Теперь, связанный с примером вашего кода, ваша проблема связана с генерированием и использованием сертификата.
-
Вы не предоставляете эмитента для своего сертификата, и таким образом ему нельзя доверять - это причина исключения RemoteCertificateChainErrors. Я предлагаю создать самоподписанный сертификат для целей разработки, указав параметр -r для makecert.
-
Чтобы быть уверенным, что сертификат должен быть самоподписанным и помещен в надежное место в хранилище сертификатов Windows или должен быть связан с цепочкой подписей с уже доверенным центром сертификации. Поэтому вместо параметра -ss My, который поместит сертификат в личный магазин, используйте -ss root, который поместит его в доверенные корневые центры сертификации, и ему будет доверено на вашем компьютере (из кода я предполагаю, что ваш клиент запущен на том же компьютере с сервером, а также на нем создается сертификат).
-
Если вы укажете выходной файл для makecert, он будет экспортировать сертификат как .cer, но этот формат содержит только открытый ключ, а не закрытый ключ, необходимый серверу для установления SSL-соединения. Самый простой способ - прочитать сертификат из хранилища сертификатов Windows в код сервера. (Вы также можете экспортировать его из магазина в другом формате, который позволяет хранить закрытый ключ, как описано здесь Экспортировать сертификат с закрытым ключом и читать, что файл в код сервера).
Подробные сведения о параметрах makecert, используемых здесь Средство создания сертификата (Makecert.exe)
В заключение вашему коду нужны следующие изменения для запуска (проверены с последними обновлениями кода):
- Используйте следующую команду для создания сертификата:
makecert -sr LocalMachine -ss root -r -n "CN = localhost" -личный обмен -sk 123456
- Прочитайте сертификат из хранилища сертификатов Windows вместо файла (для простоты этого примера), поэтому замените
serverCertificate = X509Certificate.CreateFromCertFile(сертификат);
в коде сервера с помощью:
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];
}
Не забудьте заменить "CN = localhost" темой сертификата, который вы собираетесь использовать, если вы позже измените код (в этой ситуации должно быть такое же значение, как опция -n, переданная в makecert). Также рассмотрите использование имени машины, которое запускает сервер вместо localhost в теме сертификата сервера.
Ответ 2
Сертификат сервера CN должен быть точно таким же, как имя домена сервера. Я полагаю, в вашем случае общее имя должно быть "localhost" (без кавычек).
Важно: наверняка, как вы могли бы прочитать в других ответах, никогда не используйте CN="localhost"
в процессе производства.
Ответ 3
Во-первых, не создавайте сертификат с темой "CN = localhost" или эквивалентом. Он никогда не будет использоваться в производстве, поэтому не делайте этого. Всегда выдавайте его на имя компьютера, например. CN = "mycomputer" и использовать имя хоста при подключении к нему, а не localhost. Вы можете указать несколько имен, используя расширение "альтернативное имя субъекта", но makecert
не поддерживает его.
Во-вторых, при выдаче сертификата SSL сервера вам необходимо добавить OID аутентификации сервера к расширению расширения ключа (EKU) сертификата. Добавьте параметр -eku 1.3.6.1.5.5.7.3.1
в makecert
в вашем примере. Если вы хотите выполнить аутентификацию сертификата клиента, используйте OID для проверки подлинности клиента 1.3.6.1.5.5.7.3.2.
Наконец, сертификат по умолчанию, созданный makecert, использует MD5 как его алгоритм хеширования. MD5 считается небезопасным и, хотя это не повлияет на ваше тестирование, привыкнуть к использованию SHA1. Добавьте -a sha1
к параметрам makecert
выше, чтобы заставить SHA1. Размер ключа по умолчанию также должен быть увеличен с 1024 бит до 2048 бит, но вы получите эту идею.
Ответ 4
Чтобы заставить это работать с WCF, необходимо сначала создать самозаверяющий сертификат корневого центра, а затем использовать его для создания сертификата для localhost.
Я думаю, что то же самое может быть применимо и к вашему проекту, пожалуйста, ознакомьтесь с этой статьей Как создать временные сертификаты для использования во время разработки для деталей.
Ответ 5
Вы пробовали:?
Создайте сертификат для полного доменного имени, такого как example.net
(полезно использовать example.net
, example.com
или example.org
для чего-либо, что преднамеренно не настоящее имя), или имя, которое будет использоваться в режиме реального времени если это единственный сайт, и вы знаете, что это будет.
Обновите файл хостов, чтобы он использовал 127.0.0.1 для этого имени.
Ответ 6
Что касается вашего обновления:
Один из конструкторов SslStream позволяет вам предоставить RemoteCertificateValidationCallback delegate. Вы должны уметь поставить точку останова в том методе, который вы указали, чтобы узнать, какая фактическая ошибка вы получаете. Проверьте значение SslPolicyErrors, отправленное в.