Strona używa cookies (ciasteczek). Dowiedz się więcej o celu ich używania i zmianach ustawień. Korzystając ze strony wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki.    X

Połączenia SSL w c#

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 :)

 

windows bezpieczeństwo programowanie

Komentarze

0 nowych
floyd   14 #1 27.12.2012 23:31

Uwielbiam teksty z takimi dopiskami jak:
'.... prawda, że nic trudnego?'
'..... prawda, że proste?'
Mnie się zawsze wydawało, że to czy coś jest proste czy też nie jest rzeczą względną i dla różnych osób może wyglądać różnie.
Pamiętam z uczelni jak niektórzy wykładowcy nagminnie używali tego typu zwrotów czym skutecznie zamykali usta słuchaczom, bo jeżeli coś jest proste to zadając pytanie można wyjść na głupa. :)
No, a poza tym jeżeli coś jest proste zdaniem tłumaczącego, to oczywistym jest, że zwalnia go to od bardziej dogłębnego tłumaczenia. :)

koneton   6 #2 28.12.2012 10:13

Jakiej wersji protokołu SSL używasz łącząc się w ten sposób?

koneton   6 #3 28.12.2012 10:13

Jakiej wersji protokołu SSL używasz łącząc się w ten sposób?

budda86   9 #4 28.12.2012 10:48

Naprawdę pisząc w C# w aż tak koszmarny sposób formatuje się kod i nazywa zmienne (m_Stream itd.)?

  #5 28.12.2012 13:52

@up
A na autora popatrzyłeś? :)

kadet90   5 #6 28.12.2012 14:23

@Description_1 teraz patrz, na autora tam, a teraz tu, i co widzisz? Wpis powstał jako swoisty tutorial dla początkujących, bo takie połączenia sprawiają często problemy z tego co widzę.

@budda86 m_nazwa lub po prostu _nazwa przyjmuje się dla wszystkich właściwości/metod prywatnych bądź chronionych, i ciekawe bo ucięło mi prawie wszystkie puste linie.

@koneton wybierasz przy połączeniu w metodzie AuthenticateAsClient, domyślnie może się połączyć lub z SSL3 lub TLS1.0 :P

@RaveStar - W kodzie produkcyjnym, owszem używam j. angielskiego, w kodzie instruktażowym, który uwaga, jest dla Polaków, nie widzę sensu używania języka innego niż nasz ojczysty.

@floyd :D Faktycznie, nadużywam tych zwrotów.

kadet90   5 #7 28.12.2012 14:29

@RaveStar zobaczę, co by nie było, dziękuje za link.

alucosoftware   7 #8 29.12.2012 11:21

@anios
"Pamiętaj, żebyś" spojrzał także do treści książki a nie jedynie na jej tytuł... przy okazji spójrz na jej autora i zrewiduj swoje spostrzeżenia na temat zielonego pojęcia o programowaniu.

Autor edytował komentarz.
xm2   3 #9 29.12.2012 11:33

@anios
Wow, chyba się zagalopowałeś w temat, którego nie znasz.

Po pierwsze jedyne odniesienie do wyrażenia Agile jest w tytule książki, która sama w sobie jest luźno powiązana z samymi metodykami Agile, co oznacza, że odnosisz się do książki, której nie znasz (nawet spisu treści, czy opisu).

Agile nie ma też nic wspólnego z czytelnością kodu, czy szybkością jego wykonywania. To są metodyki zwinnego wytwarzania oprogramowania, zajmują się procesem wytwarzania softu, a nie samym pisaniem kodu. To jest bardzo nie to samo, to nie ta płaszczyzna. Tworzenie oprogramowania mułowatego i błędogennego nie ma z tym NIC wspólnego.

Ale najlepszy jest tekst o ludziach, którzy wprowadzali Agile. Jeżeli uważasz, że Beck, Cunningham, Fowler albo C. Martin nie mają zielonego pojęcia o programowaniu i procesie jego wytwarzania to szczerze gratuluję podejścia ;)

W sumie to jedyne z czym się zgodzę, to pewna część wydźwięku Twojej wypowiedzi - też nie pochwalam przesadzonego puryzmu w takich tekstach jak ten kadet90, któremu zamiast bury za kod, należy się pochwałą za ciekawy temat.