Narzędzia użytkownika

Narzędzia witryny


Pasek boczny


O mnie


Dydaktyka:

Feedback


sk2:openssl_ssl_api

Generowanie kluczy i certyfikatów co ćwiczeń, TLS z powłoki

Klucze i certyfikaty

Do wygenerowania klucza prywatnego CA i certyfikatu CA oraz klucza prywatnego serwera i podpisanego przez wcześniej wygenerowane CA certyfikatu serwera użyj tego skryptu.

Zadanie 1 Wygeneruj potrzebne pliki za pomocą powyższego skryptu. Wyświetl informacje o certyfikatach komendą:
openssl x509 -noout -text -certopt no_sigdump,no_pubkey -in <scieżka do certyfikatu>

Gotowe narzędzia do tworzenia prostych połączeń TLS w powłoce

Podobną rolę jak program nc / netcat dla połączeń szyfrowanych spełniają narzędzia openssl s_client i openssl s_server.
Również program socat pozwala na tworzenie połączeń szyfrowanych.

Klient SSL - openssl s_client / socat

openssl s_client [-verifyCAfile ca.crt] [-crlf] [-quiet|-brief] [-connect] put.poznan.pl:443
np: openssl s_client -brief mail.put.poznan.pl:imaps
-verifyCAfile <CA cert> dodaje plik CA jako zaufany do sprawdzania certyfikatu serwera
-crlf zamienia w danych odczytanych ze standardowego wejścia \n na \r\n
-quiet wyłącza zupełnie komunikaty diagnostyczne, -brief skraca komunikaty diagnostyczne; bez tych przełączników są pokazywane długie komunkty
[-connect] <host>:<port> wskazuje gdzie się połączyć; słowo kluczowe -connect można pominąć jeśli <host>:<port> jest na końcu komendy

socat [open]ssl:put.poznan.pl:443,verify=0[,crlf] stdio
np. socat ssl:mail.put.poznan.pl:imaps,verify=0 stdio
verify=0 wyłącza sprawdzanie certyfikatu serwera
crlf zamienia w wysyłanych danych \n na \r\n

Zadanie 2 Połącz się z serwerem stron internetowych i otrzymaj od niego odpowiedź.
Kto podpisał certyfikat tego serwera? Czy certyfikat został zweryfikowany poprawnie?

Serwer SSL - openssl s_server / socat

Pamiętaj że do stworzenia prostego serwera połączeń szyfrowanych potrzebujesz1) klucza prywatnego i certyfikatu.

openssl s_server [-cert_chain chain.pem] -cert s.crt -key s.key [-quiet|-brief] [-naccept 1] -port 4000
-key <key> podaje ścieżkę do klucza prywatnego
-cert <cert> podaje ścieżkę do certyfikatu serwera
-cert_chain <chain> podaje ścieżkę do łańcucha certyfikatów które podpisały certyfikat serwera
-naccept <num> podaje po ilu połączeniach (obsługiwanych sekwencyjnie) serwer ma się zakończyć

socat openssl-listen:4000,key=server.key,cert=chain.pem,reuseaddr,verify=0[,fork] stdio
key=<key> podaje ścieżkę do klucza prywatnego
cert=<chain> podaje ścieżkę do (łańcucha) certyfikatów które podpisały certyfikat serwera
fork nakazuje socatowi się sklonować po nawiązaniu połączenia; jeden proces będzie obsługiwał nawiązane połączenie, drugi czekał na nowe; uwaga: przy łączeniu drugiego końca z stdio wszystkie procesy obsługujące połączenia będą czytać z tego samego deskryptora (dane trafią niedeterministycznie do jednego z nich)

Zadanie 3 Uruchom prosty serwer TLS używając wygenerowanych przez siebie certyfikatów.
Połącz się do niego klientem z konsoli.

Zadanie 4 Połącz się do prostego serwera TLS z przeglądarki internetowej.
Dlaczego przeglądarka uważa połączenie za niebezpieczne?
Możesz albo uruchomić serwer (z uprawnieniami administratora) na porcie 443 i w przeglądarce podać https://<nazwa domenowa>/, lub uruchomić na wybranym porcie i podać https://<nazwa domenowa>:<numer portu>/

Zadanie 5 Zainstaluj certyfikat swojego CA w przeglądarce. Następnie połącz się podając jako nazwę domenową localhost oraz lab-net-▒▒.
Czy przeglądarka teraz ufa serwerowi?

Intefejs programistyczny biblioteki OpenSSL

Wstęp do API

Biblioteki OpenSSL są oparte o abstrakcję wejścia/wyjścia BIO.
Tworząc kod używający biblioteki OpenSSL do szyfrowanych połączeń sieciowych TLS (libssl) programista musi najpierw stworzyć zwykłe połączenie TCP. Może do tego użyć albo abstrakcji BIO (klient, serwer), albo zwykłego interfejsu gniazd.
Uwaga: tutaj jest opisana ta druga opcja. Często kod przykładowych programów używa do obsługi gniazd TCP abstrakcji OpenSSL – zwracaj na to uwagę.
Następnie programista musi na już nawiązanym połączeniu TCP ustanowić połączenie TLS. W tym celu najpierw należy stworzyć "obiekt" SSL, następnie powiązać go z istniejącym połączeniem TCP (dla zwykłego interfejsu gniazd metodą SSL_set_fd).
Dalszy sposób użycia "obiektu" SSL do nawiązania bądź przyjęcia połączenia TLS jest opisany w materiałach z wykładów.

OpenSSL nie wspiera równoległego wykonywania operacji na tym samym "obiekcie" z wielu wątków (por. man openssl-threads).
W szczególności nie wolno wykonywać równolegle SSL_read i SSL_write na tym samym "obiekcie" SSL (i deweloperzy nie są chętni tego zmieniać, bo wymagałoby to dużych zmian w bibliotece – patrz ten issue).

Nie należy też zmieniać ustawień "obiektu" SSL_CTX po jego pierwszym użyciu do stworzenia "obiektu" SSL.

Linkowanie programu z biblioteką

Do połączenia skompilowanego kodu używającego OpenSSL do połączeń TLS trzeba linkować do bibliotek libssl oraz libcrypto, np:
c++ prog.cpp -o prog -lssl -lcrypto

Przy użyciu CMake należy dodać do pliku CMakeLists.txt wyszukanie biblioteki OpenSSL oraz linkowanie wybranego celu do niej:

find_package ( OpenSSL 1.1 REQUIRED )
target_link_libraries ( prog  OpenSSL::SSL )

(Bez tego na etapie linkowania zaczną pojawiać się błędy undefined reference to… z nazwami funkcji bibliotecznych OpenSSL.)

Przykładowe programy z wykładów

Klient
kod_z_wykladow__klient.c
#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netdb.h>
 
#include <openssl/ssl.h>
 
int main(int argc, char **argv) {
    SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
    SSL *ssl = SSL_new(ctx);
 
    struct hostent *he = gethostbyname(argv[1]);
 
    struct sockaddr_in sa;
    sa.sin_family = AF_INET;
    sa.sin_port = htons(atoi(argv[2]));
    memcpy(&sa.sin_addr.s_addr, he->h_addr, he->h_length);
 
    int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    connect(fd, (struct sockaddr *)&sa, sizeof(sa));
 
    SSL_set_fd(ssl, fd);
    SSL_connect(ssl);
 
    SSL_write(ssl, "Hello, Server!\n", 15);
 
    char buf[1024];
    int count = SSL_read(ssl, buf, sizeof(buf));
    write(1, buf, count);
 
    SSL_shutdown(ssl);
    close(fd);
 
    SSL_free(ssl);
    SSL_CTX_free(ctx);
 
    return 0;
}

Zadanie 6 Skompiluj powyższy kod. Uruchom klienta łącząc się do serwera uruchomionego przy użyciu openssl s_server … lub socat openssl-listen:….

Serwer
kod_z_wykladow__serwer.c
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
 
#include <openssl/ssl.h>
 
int main(int argc, char **argv) {
    SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
 
    SSL_CTX_use_certificate_chain_file(ctx, "chain.pem")                        == 1 || fprintf(stderr, "problem with chain.pem\n");
    //SSL_CTX_use_certificate_file(ctx, "server.crt", SSL_FILETYPE_PEM)         == 1 || fprintf(stderr, "problem with server.crt\n");
    SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM)            == 1 || fprintf(stderr, "problem with server.key\n");
 
    struct sockaddr_in sa = {0};
    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = INADDR_ANY;
    sa.sin_port = htons(atoi(argv[1]));
 
    int sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    bind(sfd, (struct sockaddr *)&sa, sizeof(sa))                               == 0 || fprintf(stderr, "problem with bind\n");
    listen(sfd, 10);
 
    while (1) {
        int cfd = accept(sfd, NULL, NULL);
 
        SSL *ssl = SSL_new(ctx);
        SSL_set_fd(ssl, cfd);
 
        SSL_accept(ssl);
 
        char buf[1024];
        SSL_read(ssl, buf, sizeof(buf));
 
        SSL_write(ssl, "Welcome to the SSL/TLS server!\n", 31);
 
        SSL_shutdown(ssl);
        SSL_free(ssl);
        close(cfd);
    }
}

Zadanie 7 Skompiluj powyższy kod i uruchom serwer. Upewnij się że wszystkie klucze i certyfikaty zostały poprawnie załadowane i serwer rozpoczął nasłuchiwanie na właściwym porcie.
Następnie połącz się klientem uruchomionym z terminala oraz przeglądarką internetową.

Kolejne przykładowe programy

Iteracyjny serwer z minimalną obsługą błędów

Uwaga: kompiluj poniższy kod ze standardem C++20 lub nowszym (opcja --std=c++20)

daytime_tls_server.cpp
#include <cstdio>
#include <netinet/in.h>
#include <string>
#include <sys/socket.h>
#include <unistd.h>
 
#include <openssl/ssl.h>
 
void die(const char *format, ...) {
    va_list args;
    va_start(args, format);
    vfprintf(stderr, format, args);
    va_end(args);
    exit(1);
}
 
std::string craftHttpAnswer();
 
int main(int argc, char **argv) {
    if (argc != 2)
        die("Usage: %s <port>\n", argv[0]);
 
    // Wybiera konfigurację dla serwera
    SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
    SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY);
 
    // Ładuje klucz prywatny serwera
    if (SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM) != 1)
        die("Loading Private Key from \"server.key\" failed\n");
    // Ładuje certyfikaty
    if (SSL_CTX_use_certificate_chain_file(ctx, "chain.pem") != 1)
        die("Loading certs from \"chain.pem\" failed\n");
 
    // Tworzy gniazdo i rozpoczyna nasłuchiwanie - zwykły kod z API gniazd
    int serv = socket(AF_INET, SOCK_STREAM, 0);
    const int one = 1;
    setsockopt(serv, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
    sockaddr_in sa = {};
    sa.sin_family = AF_INET;
    sa.sin_port = htons(atoi(argv[1]));
    if (bind(serv, (sockaddr *)&sa, sizeof(sa)) != 0)
        die("bind failed: %s\n", strerror(errno));
    listen(serv, 1);
 
    while (1) {
        // przyjmuje nowe połączenie
        int cli = accept(serv, 0, 0);
 
        // tworzy warstwę szyfrującą i wykonuje handshake
        SSL *encryption_layer = SSL_new(ctx);
        SSL_set_fd(encryption_layer, cli);
        if (SSL_accept(encryption_layer) != 1) {
            fprintf(stderr, "TLS handshake failed!\n");
            continue;
        }
 
        std::string answer = craftHttpAnswer();
 
        // przesyła odpowiedź
        if (SSL_write(encryption_layer, answer.c_str(), answer.length()) <= 0) {
            fprintf(stderr, "Problems with write!\n");
            continue;
        }
 
        // zamyka warstwę szyfrującą
        SSL_shutdown(encryption_layer);
 
        // zamyka gniazdo
        shutdown(cli, SHUT_RDWR);
        close(cli);
 
        SSL_free(encryption_layer);
    }
}
 
#include <chrono>
#include <format>
std::string craftHttpAnswer(void) {
    std::string content = std::format(
        "{:%Y.%m.%d %H:%M:%S}\r\n",
        std::chrono::system_clock::now());
    return std::format(
        "HTTP/2 200\r\n"
        "content-type: text/plain\r\n"
        "content-length: {}\r\n"
        "\r\n"
        "{}",
        content.length(), content);
}

Zadanie 8 Powtórz poprzednie zadanie używając kodu tego serwera.

Klient z obsługą błędów

Poniższy kod klienta:

  • obsługuje błędy, wyświetlając adekwatne komunikaty
  • weryfikuje poprawność certyfikatu przedstawionego przez serwer
  • odczytuje naraz dane ze standardowego wejścia i z połączenia TLS używając zdarzeń
  • obsługuje przypadki brzegowe wartości zwracanych przez funkcje SSL_…
tls_client.cpp
#include <cstdio>
#include <fcntl.h>
#include <netdb.h>
#include <poll.h>
#include <string>
#include <sys/socket.h>
#include <unistd.h>
 
#include <openssl/err.h>
#include <openssl/ssl.h>
 
void die(const char *format, ...) {
    va_list args;
    va_start(args, format);
    vfprintf(stderr, format, args);
    va_end(args);
    exit(1);
}
 
int main(int argc, char **argv) {
    if (argc != 3)
        die("Usage: %s <ip> <port>\n", argv[0]);
 
    // tworzy gniazdo i nawiązuje połączenie TCP
    addrinfo *res;
    if (getaddrinfo(argv[1], argv[2], 0, &res))
        die("getaddrinfo failed\n");
    int cli = socket(res->ai_family, SOCK_STREAM, 0);
    if (connect(cli, res->ai_addr, res->ai_addrlen) != 0)
        die("connect failed\n");
    freeaddrinfo(res);
 
    /* ───────────────────────────────────────────────────────────────────── */
 
    // wybiera konfigurację warstwy szyfrującej
    SSL_CTX *ctx = SSL_CTX_new(TLS_client_method()); // podstawowe ustawienia dla klienta TLS
    SSL_CTX_set_default_verify_paths(ctx); // nakazuje użyć systemowych certyfikatów CA
 
    // żeby przetestować weryfikację certyfikatu bez instalowania certyfikatu CA w systemie:
    // ustaw zmienną środowiskową MY_ROGUE_CA ma ścieżkę do własnego certyfikatu CA
    if (getenv("MY_ROGUE_CA"))
        SSL_CTX_load_verify_file(ctx, getenv("MY_ROGUE_CA"));
 
    /* ───────────────────────────────────────────────────────────────────── */
 
    // tworzy obiekt reprezentujący warstwę szyfrującą i nakazuje mu czytać/pisać do pliku gniazda
    SSL *encryption_layer = SSL_new(ctx);
    SSL_set_fd(encryption_layer, cli);
 
    // informuje bibliotekę jakiej nazwie powinien odpowiadać certyfikat
    SSL_set1_host(encryption_layer, argv[1]);
 
    // ustanawia komunikację szyfrowaną TLS (na wskazanym połączeniu TCP) wykonując handshake
    if (SSL_connect(encryption_layer) != 1)
        die("TLS handshake failed!\n");
 
    // odbiera wyniki weryfikacji poprawności otrzymanego od serwera certyfikatu
    long serverCertStatus = SSL_get_verify_result(encryption_layer);
    if (serverCertStatus != X509_V_OK)
        fprintf(stderr, "[Warning: server certificate verification failed - %s!]\n",
                X509_verify_cert_error_string(serverCertStatus));
 
    /* ───────────────────────────────────────────────────────────────────── */
 
    // fragment switcha sprawdzającego kody błędów, wspólna cześć dla read i write
    #define MY_ERROR_HANDLING_SWITCH_MISC                          \
    case SSL_ERROR_ZERO_RETURN:                                    \
        fprintf(stderr, "[Server terminated encryption layer]\n"); \
        goto close_connection;                                     \
    default:                                                       \
        fprintf(stderr, "[Some fatal error]\n");                   \
        ERR_print_errors_fp(stderr);                               \
        goto close_connection;
 
    pollfd pfds[2]{
        {.fd = 0,   .events = POLLIN},
        {.fd = cli, .events = POLLIN},
    };
 
    // zdarzenia które są potrzebne do odczytania i zapisu danych przez warstwę szyfrującą
    int readNeeds = POLLIN; // uwaga: TLS może potrzebować zrobić zapis do gniazda żeby odebrać dane
    int writeNeeds = 0; // uwaga: TLS może potrzebować zrobić odczyt do gniazda żeby wysłać dane
 
    // bufor na odbierane dane
    char recvBuffer[255];
    // bufory na wysłane dane
    std::string dataBeingWritten;
    std::string dataWaitingToBeWritten;
    // uwaga: jeśli nieblokujące SSL_write się nie powiodło bo musiałoby czekać,
    //        trzeba je powtórzyć z identycznymi argumentami; stąd dwa bufory
 
    // ustawia gniazdo w tryb nieblokujący (!)
    fcntl(cli, F_SETFL, fcntl(cli, F_GETFL) | O_NONBLOCK);
    // funkcje SSL_read i SSL_write mogą wielokrotnie wywoływać send i recv na
    // gnieździe, co przy blokującym trybie i użyciu zdarzeń mogłoby zablokować
    // program na obsłudze któregoś zdarzenia; stąd tryb nieblokujący
 
    while (1) {
        poll(pfds, 2, -1);
 
        if (pfds[0].revents) {
            char buf[256];
            int c = read(0, buf, 255);
            if (c > 0) {
                buf[c] = 0;
                dataWaitingToBeWritten += buf;
                writeNeeds |= POLLOUT;
                // dane będą wysłane dopiero w następnej iteracji pętli zdarzeń (to upraszcza kod)
            } else {
                // koniec pliku lub błąd na standardowym wejściu
                if (dataBeingWritten.empty() && dataWaitingToBeWritten.empty()) {
                    fcntl(cli, F_SETFL, fcntl(cli, F_GETFL) & ~O_NONBLOCK);
                    SSL_shutdown(encryption_layer);
                    // SSL_shutdown zamyka tylko wysyłanie danych; dane można
                    // odbierać aż druga strona nie zrobi SSL_shutdown
                    fcntl(cli, F_SETFL, fcntl(cli, F_GETFL) | O_NONBLOCK);
                    pfds[0].fd = -1;
                } else 
                    usleep(1000);
                // aktywne czekanie (powtarzana próba odczytu z zamkniętego standardowego
                // wejścia) na wepchnięcie do bufora nadawczego TCP zaszyfrowanych danych
                // przed włożeniem tam wiadomości zamknięciem warstwy szyfrującej jest tutaj
                // pozostawione dla uproszczenia kodu
            }
        }
 
        /* ───────────────────────────────────────────────────────────────────── */
 
        bool isThereAnyDataToWrite = (!dataBeingWritten.empty() || !dataWaitingToBeWritten.empty());
        if (pfds[1].revents & writeNeeds && isThereAnyDataToWrite) {
            writeNeeds = 0;
 
            if (dataBeingWritten.empty())
                std::swap(dataBeingWritten, dataWaitingToBeWritten);
 
            int result = SSL_write(encryption_layer, dataBeingWritten.c_str(), dataBeingWritten.length());
 
            // obsługa niepowodzenia funkcji SSL_write
            switch (SSL_get_error(encryption_layer, result)) {
                case SSL_ERROR_NONE:
                    break;
                case SSL_ERROR_WANT_READ:
                    writeNeeds = POLLIN;
                    break;
                case SSL_ERROR_WANT_WRITE:
                    writeNeeds = POLLOUT;
                    break;
                MY_ERROR_HANDLING_SWITCH_MISC
            }
            ERR_clear_error();
 
            // obsługa jeśli udało się wysłać dane
            if (result > 0) {
                // domyślnie SSL_write kończy się sukcesem dopiero jeśli wysłano
                // wszystkie dane, nawet jeśli gniazdo jest w trybie nieblokującym
                dataBeingWritten.clear();
                if (!dataWaitingToBeWritten.empty())
                    writeNeeds = POLLOUT;
            }
        }
 
        /* ───────────────────────────────────────────────────────────────────── */
 
        if (pfds[1].revents & readNeeds) {
            readNeeds = POLLIN;
 
            int result = SSL_read(encryption_layer, recvBuffer, 255);
            if (result > 0) {
                // odczyt się powiódł == SSL odczytał blok danych; rozmiar bloku
                // może być mniejszy bądź większy niż bufor w SSL_read.
                write(1, recvBuffer, result);
                // Jeżeli blok był większy niż bufor, to reszta danych czeka
                // w buforach warstwy szyfrującej - funkcja SSL_read może zwrócić
                // kolejną porcję danych bez wywoływania recv na gnieździe
                while (int remaining = SSL_pending(encryption_layer)) {
                    result = SSL_read(encryption_layer, recvBuffer, remaining < 255 ? remaining : 255);
                    write(1, recvBuffer, result);
                }
            } else {
                // obsługa niepowodzenia funkcji SSL_read
                switch (SSL_get_error(encryption_layer, result)) {
                case SSL_ERROR_NONE:
                case SSL_ERROR_WANT_READ:
                    break;
                case SSL_ERROR_WANT_WRITE:
                    readNeeds = POLLOUT;
                    break;
                    MY_ERROR_HANDLING_SWITCH_MISC
                }
                ERR_clear_error();
            }
        }
 
        /* ───────────────────────────────────────────────────────────────────── */
 
        pfds[1].events = readNeeds | writeNeeds;
    }
 
close_connection:
    shutdown(cli, SHUT_RDWR);
    close(cli);
    return 0;
}

Zadanie 9 Przeczytaj kod klienta. Zwróć szczególnie uwagę na komentarze – przybliżają działanie API biblioteki OpenSSL. Przetestuj kod.

[ekstra] przykład użycia BIO w kliencie TLS

ssl_client_bio.cpp
#include <cstdio>
 
#include <openssl/ssl.h>
 
// Uwaga - kod dla uproszczenia jest pozbawiony obsługi błędów!
 
int main(int argc, char **argv) {
    // przygotowuje obiekt SSL ustawia do użycia w połączeniu klienta TLS
    SSL_CTX *ssl_ctx = SSL_CTX_new(TLS_client_method());
    SSL_CTX_set_default_verify_paths(ssl_ctx);
    SSL *ssl = SSL_new (ssl_ctx);
    SSL_set_connect_state(ssl);
 
    // tworzy obiekt 'Basic Input/Output' typu połączenie sieciowe
    BIO* tcp_connection = BIO_new_connect(argv[1]);
    // łączy się do podanego adresu (jako "host:port")
    BIO_do_connect(tcp_connection);
 
    // tworzy obiekt 'Basic Input/Output' typu filtr TLS na istniejącym BIO
    BIO *tls_connection = BIO_new(BIO_f_ssl());
    // wiąże ten obiekt ze wskazanym, wcześniej skonfigurowanym obiektem SSL
    BIO_set_ssl (tls_connection, ssl, BIO_NOCLOSE);
    // podkłada pod spód tego filtra obiekt połączenia TCP
    BIO_push(tls_connection, tcp_connection);
 
    // wykonuje połączenie TLS
    BIO_do_handshake(tls_connection);
 
    // sprawdza stan certyfikatu serwera
    if(long err = SSL_get_verify_result(ssl))
        fprintf(stderr, "[cert error: %s]\n", X509_verify_cert_error_string(err));
 
    // wymienia dane po połączeniu TLS
    BIO_puts(tls_connection, "Hello World!\n");
    char buf[1024];
    BIO_get_line(tls_connection, buf, 1024);
    printf("%s", buf);
 
    // zamyka połączenie TLS
    BIO_ssl_shutdown(tls_connection);
    BIO_free(tls_connection);
 
    // połączenie TCP można tutaj dalej używać do przesyłania (nieszyfrowanych)
    // danych - w BIO_set_ssl podano żeby nie zamykać gniazda TCP (BIO_NOCLOSE)
 
    // zamyka połączenie TCP
    BIO_free(tcp_connection);
    return 0;
}

Zadania

Zadanie 10 Poniżej znajduje się program realizujący prostą sieciową bazę klucz-wartość obsługujący sekwencyjnie klientów. Program przyjmuje od klientów dwa rodzaje żądań: ustawienie klucza (format żądania: klucz wartość) i pobranie poprzednio ustawionej wartości (format żądania: klucz).
Przerób program tak, by używał połączeń TLS.

key-value_database.cpp
#include <cstdio>
#include <netdb.h>
#include <netinet/in.h>
#include <string>
#include <sys/socket.h>
#include <unistd.h>
#include <unordered_map>
 
#include <openssl/ssl.h>
 
std::unordered_map<std::string, std::string> key_value_database;
 
int main(int argc, char **argv) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <port>\n", argv[0]);
        return 1;
    }
 
    int serv = socket(AF_INET, SOCK_STREAM, 0);
 
    const int one = 1;
    setsockopt(serv, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
 
    sockaddr_in sa = {};
    sa.sin_family = AF_INET;
    sa.sin_port = htons(atoi(argv[1]));
 
    if (bind(serv, (sockaddr *)&sa, sizeof(sa))) {
        perror("bind failed");
        return 1;
    }
 
    listen(serv, 1);
 
    while (1) {
        sockaddr_in ca;
        socklen_t l = sizeof(ca);
        int cli = accept(serv, (sockaddr *)&ca, &l);
 
        char clientName[NI_MAXHOST], clientPort[NI_MAXSERV];
        getnameinfo((sockaddr *)&ca, l, clientName, NI_MAXHOST, clientPort, NI_MAXSERV, 0);
        printf("New client [%s:%s]\n", clientName, clientPort);
 
        while (1) {
            char request[1025]{};
            int count = read(cli, request, 1024);
            if (count <= 0)
                break;
            if (request[count - 1] == '\n')
                request[count - 1] = '\0';
            if (!strlen(request))
                continue;
 
            char *firstSpace = strchr(request, ' ');
            if (firstSpace != nullptr) {
                *firstSpace = '\0';
                char *key = request;
                char *value = firstSpace + 1;
                key_value_database[key] = value;
                printf("[%s:%s] assigned to '%s' value '%s'\n", clientName, clientPort, key, value);
            } else {
                char *key = request;
                std::string value;
                if (key_value_database.count(key))
                    value = key_value_database[key];
                else
                    value = "(null)";
                std::string response = std::string("[") + key + "] " + value + "\n";
                write(cli, response.data(), response.length());
                printf("[%s:%s] read from '%s' value '%s'\n", clientName, clientPort, key, value.data());
            }
        }
 
        printf("Client [%s:%s] gone\n", clientName, clientPort);
 
        shutdown(cli, SHUT_RDWR);
        close(cli);
    }
}

Zadanie 11 Zmień program z poprzedniego zadania tak, żeby program obsługiwał wielu klientów naraz używając zdarzeń. Załóż że po zgłoszeniu odpowiedniego zdarzenia przez select/pool/epoll_wait wszystkie funkcje SSL_… kończą się bez czekania (sukcesem lub błędem uniemożliwiającym dalszą komunikację).

Zadanie 12 Zmień program z poprzedniego zadania tak, by działał bez założenia o natychmiastowym zakończeniu funkcji SSL_… po zgłoszeniu odpowiedniego zdarzenia. Zauważ że w tym zadaniu musisz używać gniazd w trybie nieblokującym i analizować błędy na stosie błędów biblioteki OpensSSL (patrz przykładowy kod klienta wyżej).

1) Istnieją też rozwiązania SSL/TLS używające anonimowej wymiany DH, ale nie są one wykorzystywane z powodu nieodporności na ataki man-in-the-middle.
sk2/openssl_ssl_api.txt · ostatnio zmienione: 2024/11/22 10:32 przez jkonczak