Blog (6)
Komentarze (94)
Recenzje (0)

Połączenia SSL w c#

@kadet90Połą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 :)

Szanowna Użytkowniczko! Szanowny Użytkowniku!
×
Aby dalej móc dostarczać coraz lepsze materiały redakcyjne i udostępniać coraz lepsze usługi, potrzebujemy zgody na dopasowanie treści marketingowych do Twojego zachowania. Twoje dane są u nas bezpieczne, a zgodę możesz wycofać w każdej chwili na podstronie polityka prywatności.

Kliknij "PRZECHODZĘ DO SERWISU" lub na symbol "X" w górnym rogu tej planszy, jeżeli zgadzasz się na przetwarzanie przez Wirtualną Polskę i naszych Zaufanych Partnerów Twoich danych osobowych, zbieranych w ramach korzystania przez Ciebie z usług, portali i serwisów internetowych Wirtualnej Polski (w tym danych zapisywanych w plikach cookies) w celach marketingowych realizowanych na zlecenie naszych Zaufanych Partnerów. Jeśli nie zgadzasz się na przetwarzanie Twoich danych osobowych skorzystaj z ustawień w polityce prywatności. Zgoda jest dobrowolna i możesz ją w dowolnym momencie wycofać zmieniając ustawienia w polityce prywatności (w której znajdziesz odpowiedzi na wszystkie pytania związane z przetwarzaniem Twoich danych osobowych).

Od 25 maja 2018 roku obowiązuje Rozporządzenie Parlamentu Europejskiego i Rady (UE) 2016/679 (określane jako "RODO"). W związku z tym chcielibyśmy poinformować o przetwarzaniu Twoich danych oraz zasadach, na jakich odbywa się to po dniu 25 maja 2018 roku.

Kto będzie administratorem Twoich danych?

Administratorami Twoich danych będzie Wirtualna Polska Media Spółka Akcyjna z siedzibą w Warszawie, oraz pozostałe spółki z grupy Wirtualna Polska, jak również nasi Zaufani Partnerzy, z którymi stale współpracujemy. Szczegółowe informacje dotyczące administratorów znajdują się w polityce prywatności.

O jakich danych mówimy?

Chodzi o dane osobowe, które są zbierane w ramach korzystania przez Ciebie z naszych usług, portali i serwisów internetowych udostępnianych przez Wirtualną Polskę, w tym zapisywanych w plikach cookies, które są instalowane na naszych stronach przez Wirtualną Polskę oraz naszych Zaufanych Partnerów.

Dlaczego chcemy przetwarzać Twoje dane?

Przetwarzamy je dostarczać coraz lepsze materiały redakcyjne, dopasować ich tematykę do Twoich zainteresowań, tworzyć portale i serwisy internetowe, z których będziesz korzystać z przyjemnością, zapewniać większe bezpieczeństwo usług, udoskonalać nasze usługi i maksymalnie dopasować je do Twoich zainteresowań, pokazywać reklamy dopasowane do Twoich potrzeb. Szczegółowe informacje dotyczące celów przetwarzania Twoich danych znajdują się w polityce prywatności.

Komu możemy przekazać dane?

Twoje dane możemy przekazywać podmiotom przetwarzającym je na nasze zlecenie oraz podmiotom uprawnionym do uzyskania danych na podstawie obowiązującego prawa – oczywiście tylko, gdy wystąpią z żądaniem w oparciu o stosowną podstawę prawną.

Jakie masz prawa w stosunku do Twoich danych?

Masz prawo żądania dostępu, sprostowania, usunięcia lub ograniczenia przetwarzania danych. Możesz wycofać zgodę na przetwarzanie, zgłosić sprzeciw oraz skorzystać z innych praw wymienionych szczegółowo w polityce prywatności.

Jakie są podstawy prawne przetwarzania Twoich danych?

Podstawą prawną przetwarzania Twoich danych w celu świadczenia usług jest niezbędność do wykonania umów o ich świadczenie (tymi umowami są zazwyczaj regulaminy). Podstawą prawną przetwarzania danych w celu pomiarów statystycznych i marketingu własnego administratorów jest tzw. uzasadniony interes administratora. Przetwarzanie Twoich danych w celach marketingowych realizowanych przez Wirtualną Polskę na zlecenie Zaufanych Partnerów i bezpośrednio przez Zaufanych Partnerów będzie odbywać się na podstawie Twojej dobrowolnej zgody.