Безопасное программирование сокетов с OpenSSL и C

Задний план

В последнее время я обнаружил OpenSSL быть трудным для освоения новичком, в то время как это может быть реализовано внутри дружественных для новичков проектов, таких как программирование сокетов. После двух недель исследований я написал следующую программу. Я решил поделиться им с сообществом, чтобы выявить проблемы, которые можно исправить, и добавить ресурс для OpenSSL обучение.

Цель

Цель этой публикации — поднять обсуждение работающей программы безопасных сокетов и найти способы ее улучшения. Есть несколько тем, которые можно обсудить:

  1. Безопасность. Может ли программа справляться с реальными угрозами? Или же соединение слишком простое и его можно легко использовать?
  2. Дублированные настройки. И клиент, и сервер используют одни и те же настройки, например сообщения об ошибках. В реальной программе клиент и сервер также будут использовать одни и те же правила связи. Моей первой мыслью было использовать MongoDB для хранения настроек, но было очень медленно и беспорядочно импортировать настройки из базы данных. Как с этим справиться?
  3. Любые другие советы и исправления.

Описание

Программа открывает простое безопасное соединение между клиентом и сервером. Сервер может обрабатывать несколько клиентов одновременно, отправляя фиксированные эхо-сообщения. "OK!". Каждому клиенту разрешено отправлять собственные сообщения или выходить из программы.

Для работы серверной программы требуется корневой сертификат и сертификат сервера. Он может их генерировать и выполнять openssl команды для вас: sudo ./server install root. OpenSSL должен быть установлен.

kali@DESKTOP-DFHKO83:/mnt/c/Users/Ori/desktop/crev/server$ sudo ./server 8080
Waiting for clients to connect...
{127.0.0.1, 54872}: this is client1! || {sent}: OK!
{127.0.0.1, 54873}: this is client2 || {sent}: OK!

Структура проекта

В client и server программы писались точно так же. Поэтому только server предоставляется исходный код. В client исходный код доступно на github.

.
├── client
│   ├── build
│   │   └── CMakeLists.txt
│   ├── connection.c
│   ├── exit.c
│   ├── include
│   │   ├── connection.h
│   │   ├── exit.h
│   │   └── settings.h
│   └── main.c
└── server
    ├── build
    │   └── CMakeLists.txt
    ├── connection.c
    ├── exit.c
    ├── include
    │   ├── connection.h
    │   ├── exit.h
    │   └── settings.h
    └── main.c

Исходный код сервера

CMakeLists.txt

cmake_minimum_required(VERSION 3.13)
project(server C)
set(CMAKE_C_STANDARD 11)
add_executable(server main.c exit.c connection.c)
target_link_libraries(server ssl crypto)
sudo apt-get install openssl
cmake build
make
sudo ./server install root
sudo ./server 8080

connection.h

int bindAddr(int port);
void useCertificate(SSL_CTX* ctx, char* CertFile, char* KeyFile);
SSL_CTX* InitServerCTX(void);
void startServer(SSL_CTX *ctx, int *server, int port);
void closeSSL(SSL* ssl);
void closeServerSSL(int server, SSL_CTX* ctx);

exit.h

// --- exit integers ---
enum exit {
    EXIT_OK,
    NOT_ROOT,
    ARGS_ERR,
    SSL_MISSING,
    CRT_VERIFICATION,
    BIND_ERR,
    LISTEN_ERR,
    ABORT,
    KEYS_NOT_MATCH,
    CRT_USE_ERR,
    PKEY_USE_ERR,
    SYS_CMD_ERR,
    ROOT_CRT_ERR,
    CRT_UPDATE_ERR,
    SERVER_CRT_ERR,
    LAST
};


int report(int err);
int reportExit(int err);
const char* getExitName(enum exit ex);
void printReportsInfo();

settings.h

// --- global settings ---
#define FAIL -1
#define SSL_PKG "openssl"
#define SH_RP_ARG "reports"
#define REPORT_PATH "report/err.rprt"
#define CRT_PATH "certificates"
#define ROOT_PATH "/usr/share/ca-certificates/extra"
#define ROOT_FNAME "rootsslzp"
#define SERVER_CRT_FNAME "server"
#define ROOT_REQ_CNF ""/C=IL/ST=Israel/L=Israel/O=Ori David/OU=root-req-unit/CN=root-crt/emailAddress=test@gmail.com/""
#define SERVER_REQ_CNF ""/C=IL/ST=Israel/L=Israel/O=Ori David/OU=server-req-unit/CN=server-crt/emailAddress=test@gmail.com/""
#define SCRIPTS_PATH "scripts"
#define ROOT_CRT_WARNING "No root certificate. Suggestions:n    1. RUN: sudo ./server install rootn    2. Install a root ceritificate as '/usr/share/extra/rootsslzpcert.crt'n"
char *servercert, *serverkey;

connection.c

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <malloc.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <resolv.h>
#include "openssl/ssl.h"
#include "openssl/err.h"
#include "include/settings.h"
#include "include/connection.h"
#include "include/exit.h"

int bindAddr(int port) {
    // --- bind address to socket ---
    struct sockaddr_in addr;
    int sd = socket(PF_INET, SOCK_STREAM, 0); // --- server descriptor ---
    bzero(&addr, sizeof(addr)); // --- memset addr to zero --
    addr.sin_family = AF_INET; // --- IPv4 address family
    addr.sin_port = htons(port); // --- convert to network short byte order ---
    addr.sin_addr.s_addr = INADDR_ANY; // --- set the IP of the socket / sin_addr is an union ---
    if (bind(sd, (struct sockaddr*)&addr, sizeof(addr)) != 0) reportExit(BIND_ERR); // --- bind address ---
    if (listen(sd, 10) != 0) reportExit(LISTEN_ERR); // --- set listen queue ---
    return sd; // -- return descriptor ---
}

void useCertificate(SSL_CTX* ctx, char* CertFile, char* KeyFile) {
    // --- use server certificate ---
    if (SSL_CTX_use_certificate_file(ctx, CertFile, SSL_FILETYPE_PEM) <= 0) reportExit(CRT_USE_ERR);
    else if (SSL_CTX_use_PrivateKey_file(ctx, KeyFile, SSL_FILETYPE_PEM) <= 0) reportExit(PKEY_USE_ERR);
    else if (!SSL_CTX_check_private_key(ctx)) reportExit(KEYS_NOT_MATCH); // --- check private key ---
}

SSL_CTX* InitServerCTX(void) {
    // --- create server ssl context ---
    OpenSSL_add_all_algorithms();  // --- set cryptos ---
    SSL_load_error_strings();   // --- set error messages ---
    const SSL_METHOD *method = TLS_server_method();  // --- create server method ---
    SSL_CTX *ctx = SSL_CTX_new(method);   // --- create server context ---
    if (ctx == NULL) ERR_print_errors_fp(stderr);
    else return ctx;
    reportExit(ABORT);
}


void closeSSL(SSL* ssl) {
    // --- close client ssl ---
    SSL_free(ssl);
    close(SSL_get_fd(ssl));
}

void closeServerSSL(int server, SSL_CTX* ctx) {
    // --- close server ssl ---
    close(server);
    SSL_CTX_free(ctx); // --- release context ---
}

exit.c

#include <stdio.h>
#include <stdlib.h>
#include "include/exit.h"
#include "include/settings.h"
#include <sys/socket.h>

int report(int err) {
    // --- create report file ---
    char cmd[128];
    sprintf(cmd, "touch %s && echo %i > %s", REPORT_PATH, err, REPORT_PATH);
    system(cmd);
    printf("nEXIT: %inINFO: ./server reportsn", err);
}

int reportExit(int err) {
    // --- report error and exit --
    report(err);
    exit(-1);
}

const char* getExitName(enum exit ex) {
    // --- convert exit integer to string ---
   switch (ex)
   {
      case EXIT_OK: return "EXIT OK";
      case NOT_ROOT: return "NOT ROOT";
      case ARGS_ERR: return "ARGS ERR";
      case SSL_MISSING: return "SSL MISSING";
      case CRT_VERIFICATION: return "CRT VERIFICATION";
      case BIND_ERR: return "BIND ERR";
      case LISTEN_ERR: return "LISTEN ERR";
      case ABORT: return "ABORT";
      case KEYS_NOT_MATCH: return "KEYS NOT MATCH";
      case CRT_USE_ERR: return "CRT USE ERR";
      case PKEY_USE_ERR: return "PKEY USE ERR";
      case SYS_CMD_ERR: return "SYS CMD ERR";
      case ROOT_CRT_ERR: return "ROOT CRT ERR";
      case CRT_UPDATE_ERR: return "CRT UPDATE ERR";
      case SERVER_CRT_ERR: return "SERVER CRT ERR";
   }
}

void printReportsInfo() {
    // --- print exit enum strings ---
    for (int i = EXIT_OK; i < LAST; i++) printf("[%i] %sn", i, getExitName(i));
    reportExit(EXIT_OK);
}

main.c

// ---libraries---
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <malloc.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <resolv.h>
#include "openssl/ssl.h"
#include "openssl/err.h"
#include "include/settings.h"
#include "include/exit.h"
#include "include/connection.h"

// ---mkdirs strings ---
char mkdirs[512];

// --- SSL client ---
typedef struct Client {
    SSL *ssl;
    int d;
    struct sockaddr_in addr;
    socklen_t len;
} Client;


// ---functions---
void createDirs();
int listPackage(char pkg[]);
int isRootCrt();
void updateCA();
void writeRootCrt();
void writeCertificate();
void acceptConnections(int server, SSL_CTX *ctx);
void handleClient(int server, Client *client, SSL_CTX *ctx);

int main(int argc, char *argv[]){
    // --- main --
    sprintf(mkdirs, "report,/usr/share/ca-certificates/extra,%s", CRT_PATH); // --- directories str ---
    createDirs(); // --- create directories ---
    if (argc == 1) reportExit(ARGS_ERR);
    if (argc == 2 && strcmp(argv[1], "reports") == 0) printReportsInfo();
    if (argc == 3 && strcmp(argv[1], "install") == 0 && strcmp(argv[2], "root") == 0) writeRootCrt(); // --- install root certificate ---
    if (getuid() != 0) reportExit(NOT_ROOT); // --- sudo required ---
    if (!listPackage(SSL_PKG)) reportExit(SSL_MISSING); // --- openssl should be installed ---
    if (argc != 2) {
        printf("Usage: %s <server> <port>", argv[0]); // --- wrong usage ---
        reportExit(EXIT_OK);
    }
    if (!isRootCrt()) {
        printf("%sn", ROOT_CRT_WARNING); // --- no root certificate ---
        reportExit(EXIT_OK);
    }
    writeCertificate(); // --- create server certificates ---
    // --- init SSl and server context ---
    SSL_library_init(); // --- init ssl lib ---
    SSL_CTX *ctx = InitServerCTX(); // --- create ssl context ---
    useCertificate(ctx, servercert, serverkey); // --- load sever certificates from settings ---
    int server = bindAddr(atoi(argv[1])); // --- bind address ---
    acceptConnections(server, ctx); // --- accept connection from clients ---
}

void createDirs() {
    // --- create directories --
    char cmd[64], delim[] = ",";
    char *ptr = strtok(mkdirs, delim); // --- split directories str ---
    while(ptr != NULL) {
        sprintf(cmd, "/bin/mkdir -p %s", ptr); // --- system command ---
        if (system(cmd) == FAIL) reportExit(SYS_CMD_ERR); // --- make directory ---
        ptr = strtok(NULL, delim); // --- split next ---
    }
}

int listPackage(char pkg[]) {
    // --- check if a package is installed on the system ---
    char cmd[128], out[256], pkg_str[32];
    sprintf(cmd, "/bin/dpkg -l | grep %s", pkg); // --- system command ---
    FILE *f = popen(cmd, "r"); // --- execute command ---
    if (f == NULL) reportExit(SYS_CMD_ERR); // --- faild to execute dpkg ---
    while (fgets(cmd, sizeof(cmd), f) != NULL) strcat(out, cmd); // --- copy output to buffer ---
    sprintf(pkg_str, "ii  %s", pkg); // --- package info string ---
    return strstr(out, pkg_str) != NULL;
}

int isRootCrt() {
    // --- check if there's a root certificate under ROOT_PATH
    char rootcert[128];
    sprintf(rootcert, "%s/%scert.crt", ROOT_PATH, ROOT_FNAME); // --- system command ---
    FILE *f = fopen(rootcert, "r"); // --- open root certificate ---
    return f != NULL;
}

void updateCA() {
    // --- update trustred certificates ---
    char cmd[512];
    strcat(cmd, "sudo update-ca-certificates && sudo dpkg-reconfigure ca-certificates"); // --- system command ---
    if (system(cmd) == FAIL) reportExit(CRT_UPDATE_ERR); // --- update certificates ---
}

void writeRootCrt() {
    // --- create a root CA certificate ---
    char rootkey[128], rootreq[128], rootcert[128], cmd[512], *cmdp = cmd;
    // --- keys path ---
    sprintf(rootkey, "%s/%skey.pem", ROOT_PATH, ROOT_FNAME);
    sprintf(rootreq, "%s/%sreq.pem", ROOT_PATH, ROOT_FNAME);
    sprintf(rootcert, "%s/%scert.crt", ROOT_PATH, ROOT_FNAME);
    // --- create system command ---
    cmdp += sprintf(cmdp, "openssl req -newkey rsa:2048 -sha1 -nodes -keyout %s -out %s -subj %s", rootkey, rootreq, ROOT_REQ_CNF);
    sprintf(cmdp, " && openssl x509 -req -in %s -sha1 -signkey %s -out %s", rootreq, rootkey, rootcert);
    if (system(cmd) == FAIL) reportExit(ROOT_CRT_ERR); // --- generate keys ---
    updateCA(); // --- Install CA certificate as trusted certificate ---
    writeCertificate();
    reportExit(EXIT_OK);
}


void writeCertificate() {
    // --- create server certificate under certificates folder
    char scpath[128], cmd[1024], *cmdp = cmd, basepath[64], rootkey[128];
    // --- keys path ---
    sprintf(rootkey, "%s/%skey.pem", ROOT_PATH, ROOT_FNAME);
    sprintf(basepath, "%s/%s", CRT_PATH, SERVER_CRT_FNAME);
    sprintf(scpath, "%s/%scert.pem", CRT_PATH, SERVER_CRT_FNAME);
    FILE *f = fopen(scpath, "r"); // --- search for a server certificate ---
    if (f == NULL) {
        // --- system command: generate server certificate ---
        cmdp += sprintf(cmdp, "/bin/openssl req -newkey rsa:2048 -sha256 -nodes -keyout %skey.pem -out %sreq.pem -subj %s", basepath, basepath, SERVER_REQ_CNF);
        sprintf(cmdp, " && sudo openssl x509 -req -days 3650 -sha256 -extensions v3_ca -in %sreq.pem -CA /etc/ssl/certs/%scert.pem -CAkey %s -CAcreateserial -out %s", basepath, ROOT_FNAME, rootkey, scpath);
        if (system(cmd) == FAIL) reportExit(SERVER_CRT_ERR);
    }
    // --- allocate global keys ---
    servercert = malloc(128);
    serverkey = malloc(128);
    // --- save keys ---
    strcpy(servercert, scpath);
    sprintf(serverkey, "%skey.pem", basepath);
}


void acceptConnections(int server, SSL_CTX *ctx) {
    // --- wait and create new clients ---
    Client *client;
    printf("Waiting for clients to connect...n");
    while (1) {
        client = malloc(sizeof(Client));
        client->len = sizeof(client->addr);
        client->d = accept(server, (struct sockaddr*)&(client->addr), &(client->len)); // --- client descriptor ---
        client->ssl = SSL_new(ctx); // --- hold data for the SSL cocnnection ---
        SSL_set_fd(client->ssl, client->d); // --- assigns a socket to a SSL structure ---
        // --- wait for a SSL client to initiate the handshake ---
        if (SSL_accept(client->ssl) != FAIL)
            if (!fork()) { handleClient(server, client, ctx); }
    }
}

void handleClient(int server, Client *client, SSL_CTX *ctx) {
    // --- read messages and send echo ---
    int bytes;
    char buff[1024], echo[] = "OK!";
    while (1) {
        buff[0] = ''; // --- clear buffer ---
        bytes = SSL_read(client->ssl, buff, 1024); // --- read message ---
        if (bytes != 0) {
            // --- show [client, message] ---
            printf("{%s, %d}: %s || {sent}: %sn",inet_ntoa(client->addr.sin_addr), ntohs(client->addr.sin_port), buff, echo);
            SSL_write(client->ssl, echo, strlen(echo)); // --- send echo ---
        }
        else break; // --- end connection with client ---
    }
    // --- close client ---
    close(client->d);
    SSL_free(client->ssl);
}

```

1 ответ
1

В произвольном порядке:

Настройки

Вы собрали их в одном месте — отлично. Их изменение требует перекомпиляции — не очень хорошо. Подумайте о том, чтобы переместить их в файл настроек.

Конфликты имен

enum exit, хотя технически не в том же пространстве имен, что и exit() от stdlib.h, напрасно сбивает с толку. Наверное, назовите это как-нибудь иначе.

Форматирование

Ваш стиль кода очень … сдавлен. Вам нужно добавить несколько пустых строк внутри функций; в настоящее время их вообще нет. Наверное, после if(), тоже.

Ваш --- в комментариях не помогает; это просто визуальный шум.

Вы используете два разных стиля указателя:

SSL_CTX* InitServerCTX
SSL_CTX *ctx

Я предпочитаю второй, но в любом случае вы должны выбрать один.

Ошибка поиска

getExitName можно заменить одним поиском в массиве строк, так как перечисление ошибок является непрерывным.

Безопасность

У вас есть буферы фиксированного размера повсюду, и, как и в «старые плохие времена», доступ к ним часто уязвим для атак переполнения, как в

sprintf(mkdirs
sprintf(cmd

Вам необходимо проверить их, заменив такие вызовы вызовами, которые учитывают размер буфера.

Взаимодействие с оболочкой

Вам нужно переосмыслить report(). system несет в себе кучу рисков и проблем, которых можно легко избежать, поскольку все, что вы делаете, touch, и файл записывается в REPORT_PATH, просто используйте прямой CI / O, не касаясь оболочки.

Также рассмотрите режим добавления вместо режима усечения для своего отчета.

Примерно так же ваш призыв к dpkg — где доступ к оболочке, пожалуй, все еще самый удобный метод — не следует grep. Просто сделай сам.

openssl определенно имеет привязки C, которые будут более надежными, быстрыми и безопасными, чем вызов openssl команда через оболочку.

Сахар инициализации C99

Ваш struct sockaddr_in addr Для инициализации было бы полезно использовать инициализацию структурного литерала в стиле C99, которая также устраняет вызов bzero.

Обсуждение

Не было бы быстрее компилировать настройки в каждом обновлении, [than] читать их из файла при каждом исполнении?

Подумайте о своих возможных конечных пользователях. Им даже не нужен ваш исходный код на жестком диске, если они, как Debian, устанавливают двоичные пакеты. Приложение с настройками в заголовке будет выполняться «быстрее» (на несколько микросекунд), чем приложение, которое загружает свои настройки из файла; но двадцать секунд усилий по изменению файла настроек быстрее, чем то, что для некоторых пользователей потребовало бы многих минут труда, чтобы настроить среду разработки только для изменения строки.

Проблема безопасности, о которой вы упомянули, называется уязвимостью переполнения буфера, я прав?

https://en.wikipedia.org/wiki/Buffer_overflow

Кроме того, у меня было ощущение, что взаимодействие с оболочкой в ​​отношении openssl неверно, но «пользователь» в конечном итоге использует оболочку, не так ли?

См. Например эта рекомендация. Это вопрос «что возможно». Оболочка — это много (много, много) более сложное приложение, чем ваше, поэтому включение его значительно расширяет область обнаружения ошибок, уязвимостей безопасности и межсистемной несовместимости.

  • 1

    Отличный ответ! Спасибо 🙂 Несколько последующих обсуждений, прежде чем я приму. Не было бы быстрее компилировать настройки при каждом обновлении, читать ли их из файла при каждом выполнении? Проблема безопасности, о которой вы упомянули, называется уязвимостью переполнения буфера, я прав? Кроме того, у меня было ощущение, что взаимодействие с оболочкой касается openssl неправильно, но «пользователь» в конце концов использует оболочку, не так ли? Я думаю, бегу ./server install root это ленивый способ создания сертификатов без каких-либо openssl знания.

    — Ори Дэвид


Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *