Blog (6)
Komentarze (94)
Recenzje (0)
@kadet90Połączenia SSL w c#

Połączenia SSL w c#

27.12.2012 20:09

Widzę, że na wpisy o programowaniu jest branie, więc i ja dodam od siebie cegiełkę, może ktoś już widział ten wpis w internecie, bo aż tak nowy nie jest ale i tu go udostępnię :)

Pewnie część z Was będzie musiała kiedyś napisać jakaś aplikację, która powinna obsługiwać szyfrowanie SSL.

Jako przykład pokażę prostego klienta https, który będzie pobierał stronę z serwera a następnie wyświetlał jej kod w oknie konsoli. Wszystko za pomocą połączenia szyfrowanego.

Na dobry początek utwórzmy klasę naszego klienta:


using System;	
using System.Collections;
using System.Net;
using System.Net.Sockets;
using System.Net.Security; // Obsługa połączenia
using System.Security.Cryptography.X509Certificates;
using System.Security.Authentication; // Obsługa certyfikatów
using System.Text;
using System.IO;

namespace SSLClient
{
    class Client
    {
        public string Server { get; set; }
        public int Port { get; set; }
        
        private SslStream m_Stream;
        private TcpClient m_Client;
        public Client(string server, int port = 443)
        {
            Server = server;
            Port = port;
        }
    }
}

Skoro mamy już podstawową klasę klienta, dodajmy jej metodę od łączenia, która zainicjuje połączenie oraz włączy szyfrowanie:


public void Connect()
{
    m_Client = new TcpClient(Server, Port);
    InitSSL();
}

public void InitSSL()
{
    m_Stream = new SslStream(
        m_Client.GetStream(),   // Strumień klienta na którym ma być oparty strumień SSL
        false,
        new RemoteCertificateValidationCallback(ValidateServerCertificate), // metoda sprawdzajaca poprawność certyfikatu
        null // Metoda wybierająca certyfikat
    );
    try
    {
        // Autoryzowanie klienta na serwerze.
        m_Stream.AuthenticateAsClient(Server); // Podany serwer musi być taki sam jak ten dla którego wystawiony jest certyfikat.
    }
    catch(AuthenticationException e) // O nie, coś poszło nie tak!
    {
        Console.WriteLine("Problem podczas autoryzacji: {0}", e.Message);
        if (e.InnerException != null)
            Console.WriteLine("Wewnętrzny błąd: {0}", e.InnerException.Message);
        Console.WriteLine("Wystąpił problem podczas autoryzacji. Zamykam połączenie.");
        m_Client.Close(); // Zamknięcie połączenia.
    }
}

Jak widzimy, aby móc używać szyfrowanego połączenia musimy skorzystać z szyfrowanych połączeń musimy utworzyć strumień Ssl znajdujący się w System.Net.Security. Po więcej informacji na temat samego strumienia (czyt. Jego dokumentacje) zapraszam do MSDN. Który parametr odpowiada, za co wyjaśniłem w komentarzach, więc nie widzę powodu by robić to tutaj drugi raz. :)

W podanym powyżej kodzie warto też zwrócić uwagę na metodę sprawdzającą certyfikat, którą sami definiujemy, więc mamy pełną kontrolę nad tym, komu zaufamy a, komu nie. Metoda ta ma następujący prototyp:


bool ValidateServerCertificate(
    object sender, // wysyłający obiekt
    X509Certificate certificate, // uzyskany certyfikat
    X509Chain chain, // łańcuch certyfikatów
    SslPolicyErrors sslPolicyErrors) // błędy podczas sprawdzania
{
    ...
}

I w moim przypadku wygląda następująco:


private bool ValidateServerCertificate( // sprawdzanie certyfikatu
    object sender,
    X509Certificate certificate,
    X509Chain chain,
    SslPolicyErrors sslPolicyErrors)
{
    if (sslPolicyErrors == SslPolicyErrors.None)
        return true;
    Console.WriteLine("Błąd certyfikatu: {0}", sslPolicyErrors.ToString());
    return false;
}

Ta sprawdza tylko i wyłącznie czy nie było żadnych błędów, więc nie jest zbyt bezpieczna, bo bez słowa przepuszcza certyfikaty podpisane przez jakieś dziwne firmy i przez siebie, ale to tylko przykład, Wy macie pełną dowolność w definiowaniu tej metody, więc zadbajcie aby przepuszczała tylko zaufane certyfikaty!

SslPolicyErrors może przyjąć następujące wartości: None – Udało się, nie ma błędów. RemoteCertificateNotAvailable – Certyfikat jest niedostępny. RemoteCertificateNameMismatch – Podana na certyfikacie nazwa nie pasuje do tej, którą podajmy my. RemoteCertificateChainErrors – Łańcuch zwrócił błędy.

Drugą ważną rzeczą jest to, że przy autoryzacji (metoda AuthenticateAsClient) adres serwera musi być ten sam, jaki, jest na certyfikacie o to nie trudno, bo najczęściej jest taki sam jak adres serwera, niestety nie zawsze.

Ostatnią już rzeczą będzie napisanie metody, która wyda zapytanie http oraz odbierze jego treść, więc do dzieła:


public string GetPage(string path = "/")
{
    StringBuilder query = new StringBuilder(); // Tworzymy sobie string buildera, którym stworzymy zapytanie HTTP
    query.Append("GET " + path + " HTTP/1.1\r\n");
    query.Append("Host: " + Server + "\r\n");
    query.Append("User-Agent: Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.1.7) Gecko/20070914 Firefox/2.0.0.7\r\n"); // wmówmy serwerowi, że jesteśmy firefoxem
    query.Append("Accept: text/html;q=0.9,text/plain;q=0.8\r\n"); // chcemy html
    query.Append("Accept-Language: pl,en-us;q=0.7,en;q=0.3\r\n"); // po polsku
    query.Append("Accept-Charset: utf-8,ISO-8859-2;q=0.7,*;q=0.7\r\n"); // w utf-8
    query.Append("Connection: close\r\n\r\n"); // po odebraniu zamknij połączenie
    m_Stream.Write(Encoding.UTF8.GetBytes(query.ToString())); // Wysyłamy zapytanie do serwera.
    // potrzebne zmienne do odbierania
    byte[] buffer = new byte[2048];
    int size = 0;
    StringBuilder reply = new StringBuilder();
    // wszystko tak samo jak w normalnym połączeniu opartym o strumień, nic trudnego.
    do
    {
        size = m_Stream.Read(buffer, 0, 2048);
        reply.Append(Encoding.UTF8.GetString(buffer, 0, size));
    } while (size > 0);
    int start = reply.ToString().IndexOf("\r\n\r\n"); // pobieramy miejsce rozpoczęcia dokumentu...
    return reply.ToString().Substring(start); // ... i zwracamy dokument
}

Jedyną rzeczą, na którą powinniśmy tu zwrócić uwagę jest to, że zapytanie wysyłamy strumieniem a nie bezpośrednio przez klienta. W zasadzie to jedyna różnica. Prototyp funkcji wysyłającej najczęściej wygląda tak:

void Stream.Write(byte[] buffer);

Tak, to wszystko, prawda, że nic trudnego? Odbieranie też jest praktycznie takie samo jakbyś to robili „normalnie”

int Stream.Read(byte[] buffer, int index, int count);

Gdzie buffer to zmienna, do której pobieramy dane z strumienia, index to miejsce rozpoczęcia a count to maksymalna liczba pobranych bajtów. Metoda ta zwraca nam ilość pobranych bajtów, które potem możemy wykorzystać do ucięcia zbędnych znaków tak jak zrobiłem to ja. Na końcu ucinamy zbędną część zapytania i zwracamy stronę w postaci stringa prawda, że proste?

Teraz jedyne, co nam pozostało, to napisać mały program który będzie pobierał stronę za pomocą naszego klienta, w moim przypadku wygląda on tak:


class Program
{
    static void Main(string[] args) // Nasz zaawansowany program :)
    {
        Console.Write("Adres serwera to: ");
        Client client = new Client(Console.ReadLine());
        client.Connect();
        Console.Write("Strona, którą chcesz pobrać to: ");
        Console.WriteLine(client.GetPage(Console.ReadLine()));
        Console.ReadKey();
    }
}

Tego chyba nie muszę objaśniać? ;) Pamiętajmy też, że nie do każdego typu serwera możemy podłączyć się od razu połączeniem szyfrowanym. Przykładem jest XMPP, w którym to, aby przejść do połączenia szyfrowanego musimy najpierw wysłać pakiet o tym informujący. Na sam koniec trochę dokumentacji:

SslStream - http://msdn.microsoft.com/en-us/library/system.net.security.sslstream.aspx TcpClient - http://msdn.microsoft.com/en-us/library/system.net.sockets.tcpclient.aspx Certyfikaty - http://msdn.microsoft.com/en-us/library/system.security.cryptography.x509certificates.aspx

Oraz pełny kod aplikacji wraz z klientem:


using System;
using System.Collections;
using System.Net;
using System.Net.Sockets;
using System.Net.Security; // Obsługa połączenia
using System.Security.Cryptography.X509Certificates;
using System.Security.Authentication; // Obsługa certyfikatów
using System.Text;
using System.IO;
namespace SSLClient
{
    class Client
    {
        public string Server { get; set; }
        public int Port { get; set; }
        
        private SslStream m_Stream;
        private TcpClient m_Client;
        public Client(string server, int port = 443)
        {
            Server = server;
            Port = port;
        }
        public void Connect()
        {
            m_Client = new TcpClient(Server, Port);
            InitSSL();
        }
        public void InitSSL()
        {
            m_Stream = new SslStream(
                m_Client.GetStream(),   // Strumień klienta na którym ma być oparty strumień SSL
                false,
                new RemoteCertificateValidationCallback(ValidateServerCertificate), // metoda sprawdzajaca poprawność certyfikatu
                null // Metoda wybierająca certyfikat
            );
            try
            {
                // Autoryzowanie klienta na serwerze.
                m_Stream.AuthenticateAsClient(Server); // Podany serwer musi być taki sam jak ten dla którego wystawiony jest certyfikat.
            }
            catch(AuthenticationException e) // O nie, coś poszło nie tak!
            {
                Console.WriteLine("Problem podczas autoryzacji: {0}", e.Message);
                if (e.InnerException != null)
                    Console.WriteLine("Wewnętrzny błąd: {0}", e.InnerException.Message);
                Console.WriteLine("Wystąpił problem podczas autoryzacji. Zamykam połączenie.");
                m_Client.Close(); // Zamknięcie połączenia.
            }
        }
        public string GetPage(string path = "/")
        {
            StringBuilder query = new StringBuilder(); // Tworzymy sobie string buildera, którym stworzymy zapytanie HTTP
            query.Append("GET " + path + " HTTP/1.1\r\n");
            query.Append("Host: " + Server + "\r\n");
            query.Append("User-Agent: Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.1.7) Gecko/20070914 Firefox/2.0.0.7\r\n"); // wmówmy serwerowi że jesteśmy firefoxem
            query.Append("Accept: text/html;q=0.9,text/plain;q=0.8\r\n"); // chcemy html
            query.Append("Accept-Language: pl,en-us;q=0.7,en;q=0.3\r\n"); // po polsku
            query.Append("Accept-Charset: utf-8,ISO-8859-2;q=0.7,*;q=0.7\r\n"); // w utf-8
            query.Append("Connection: close\r\n\r\n"); // po odebraniu zamknij połączenie
            m_Stream.Write(Encoding.UTF8.GetBytes(query.ToString())); // Wysyłamy zapytanie do serwera.
            // potrzebne zmienne do odbierania
            byte[] buffer = new byte[2048];
            int size = 0;
            StringBuilder reply = new StringBuilder();
            // wszystko tak samo jak w normalnym połączeniu opartym o strumień, nic trudnego.
            do
            {
                size = m_Stream.Read(buffer, 0, 2048);
                reply.Append(Encoding.UTF8.GetString(buffer, 0, size));
            } while (size > 0);
            int start = reply.ToString().IndexOf("\r\n\r\n"); // pobieramy index rozpoczęcia dokumentu...
            return reply.ToString().Substring(start); // ... i zwracamy dokument
        }
        private bool ValidateServerCertificate( // sprawdzanie certyfikatu
            object sender,
            X509Certificate certificate,
            X509Chain chain,
            SslPolicyErrors sslPolicyErrors)
        {
            if (sslPolicyErrors == SslPolicyErrors.None)
                return true;
            Console.WriteLine("Błąd certyfikatu: {0}", sslPolicyErrors.ToString());
            return false;
        }
    }
    class Program
    {
        static void Main(string[] args) // Nasz zaawansowany program :)
        {
            Console.Write("Adres serwera to: ");
            Client client = new Client(Console.ReadLine());
            client.Connect();
            Console.Write("Strona którą chcesz pobrać to: ");
            Console.WriteLine(client.GetPage(Console.ReadLine()));
            Console.ReadKey();
        }
    }
}

Dzięki za przeczytanie :)

Wybrane dla Ciebie
Komentarze (9)