Задний план
В последнее время я обнаружил OpenSSL
быть трудным для освоения новичком, в то время как это может быть реализовано внутри дружественных для новичков проектов, таких как программирование сокетов. После двух недель исследований я написал следующую программу. Я решил поделиться им с сообществом, чтобы выявить проблемы, которые можно исправить, и добавить ресурс для OpenSSL
обучение.
Цель
Цель этой публикации — поднять обсуждение работающей программы безопасных сокетов и найти способы ее улучшения. Есть несколько тем, которые можно обсудить:
- Безопасность. Может ли программа справляться с реальными угрозами? Или же соединение слишком простое и его можно легко использовать?
- Дублированные настройки. И клиент, и сервер используют одни и те же настройки, например сообщения об ошибках. В реальной программе клиент и сервер также будут использовать одни и те же правила связи. Моей первой мыслью было использовать
MongoDB
для хранения настроек, но было очень медленно и беспорядочно импортировать настройки из базы данных. Как с этим справиться? - Любые другие советы и исправления.
Описание
Программа открывает простое безопасное соединение между клиентом и сервером. Сервер может обрабатывать несколько клиентов одновременно, отправляя фиксированные эхо-сообщения. "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 ответ
В произвольном порядке:
Настройки
Вы собрали их в одном месте — отлично. Их изменение требует перекомпиляции — не очень хорошо. Подумайте о том, чтобы переместить их в файл настроек.
Конфликты имен
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 неверно, но «пользователь» в конечном итоге использует оболочку, не так ли?
См. Например эта рекомендация. Это вопрос «что возможно». Оболочка — это много (много, много) более сложное приложение, чем ваше, поэтому включение его значительно расширяет область обнаружения ошибок, уязвимостей безопасности и межсистемной несовместимости.
Отличный ответ! Спасибо 🙂 Несколько последующих обсуждений, прежде чем я приму. Не было бы быстрее компилировать настройки при каждом обновлении, читать ли их из файла при каждом выполнении? Проблема безопасности, о которой вы упомянули, называется уязвимостью переполнения буфера, я прав? Кроме того, у меня было ощущение, что взаимодействие с оболочкой касается
openssl
неправильно, но «пользователь» в конце концов использует оболочку, не так ли? Я думаю, бегу./server install root
это ленивый способ создания сертификатов без каких-либоopenssl
знания.— Ори Дэвид