C语言SSL socket如何实现安全通信?

99ANYc3cd6
预计阅读时长 33 分钟
位置: 首页 C语言 正文
  1. 初始化 OpenSSL
  2. 创建 TCP Socket
  3. 创建 SSL 上下文
  4. 创建 SSL 结构体
  5. 建立 SSL 连接
  6. 通过 SSL 进行加密通信
  7. 关闭和清理

下面我将分为 服务器端客户端 两部分,并提供完整的代码示例。


准备工作:安装 OpenSSL

在开始之前,请确保你的系统上安装了 OpenSSL 开发库。

  • 在 Ubuntu/Debian 上:
    sudo apt-get update
    sudo apt-get install libssl-dev
  • 在 CentOS/RHEL 上:
    sudo yum install openssl-devel
  • 在 macOS 上 (使用 Homebrew):
    brew install openssl

核心概念

  • SSL_CTX (SSL Context): SSL 上下文,它是一个结构体,包含了 SSL 会话所需的所有信息,比如密码套件列表、证书、私钥等,我们可以把它看作是一个“工厂”或“模板”,用于创建多个 SSL 结构体。
  • SSL (SSL Structure): SSL 结构体,它代表一个具体的 SSL 连接,当你需要处理一个客户端连接时,你会从 SSL_CTX 创建一个 SSL 结构体。
  • BIO (Basic I/O): OpenSSL 的 I/O 抽象层,通常我们直接使用标准的 readwrite 函数,这背后是通过 BIO 来实现的。BIO 提供了一个统一的接口来处理各种 I/O,包括文件、Socket、内存等。

客户端 示例

客户端的流程相对简单:连接服务器 -> 建立 SSL 连接 -> 加密通信。

客户端代码 (ssl_client.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#define PORT 4433
#define SERVER_IP "127.0.0.1" // 服务器 IP 地址
// 初始化 OpenSSL
void init_openssl() {
    SSL_load_error_strings();
    OpenSSL_add_ssl_algorithms();
}
// 清理 OpenSSL
void cleanup_openssl() {
    EVP_cleanup();
}
// 创建并建立 TCP Socket
int create_socket() {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("Unable to create socket");
        exit(EXIT_FAILURE);
    }
    return sock;
}
int main() {
    int sock;
    SSL_CTX *ctx;
    SSL *ssl;
    char buffer[1024] = {0};
    // 1. 初始化 OpenSSL
    init_openssl();
    // 2. 创建 SSL 上下文
    ctx = SSL_CTX_new(TLS_client_method());
    if (!ctx) {
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }
    // 3. 创建 TCP Socket
    sock = create_socket();
    // 4. 设置服务器地址并连接
    struct sockaddr_in serv_addr;
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
        perror("Invalid address/ Address not supported");
        exit(EXIT_FAILURE);
    }
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("Connection Failed");
        exit(EXIT_FAILURE);
    }
    printf("Connected to server.\n");
    // 5. 创建 SSL 结构体并关联 Socket
    ssl = SSL_new(ctx);
    SSL_set_fd(ssl, sock);
    // 6. 建立 SSL 连接 (执行 TLS 握手)
    if (SSL_connect(ssl) <= 0) {
        ERR_print_errors_fp(stderr);
    } else {
        printf("SSL connection established.\n");
    }
    // 7. 通过 SSL 进行加密通信
    // 发送数据
    char *msg = "Hello from SSL Client!";
    SSL_write(ssl, msg, strlen(msg));
    // 接收数据
    int bytes = SSL_read(ssl, buffer, sizeof(buffer) - 1);
    if (bytes > 0) {
        buffer[bytes] = '\0';
        printf("Server says: %s\n", buffer);
    }
    // 8. 关闭连接和清理
    SSL_shutdown(ssl);
    SSL_free(ssl);
    close(sock);
    SSL_CTX_free(ctx);
    cleanup_openssl();
    return 0;
}

服务器端 示例

服务器端的流程更复杂一些:创建 Socket -> 绑定 -> 监听 -> 接受连接 -> 建立 SSL 连接 -> 加密通信。

重要提示: 服务器需要证书和私钥来证明自己的身份,这里我们使用 OpenSSL 自带的测试证书,你可以从 OpenSSL 官网下载。

  1. 下载并解压 OpenSSL。

  2. 进入 apps 目录,运行以下命令生成测试证书和私钥:

    # 生成私钥
    openssl genrsa -out server.key 2048
    # 生成证书签名请求 (CSR)
    openssl req -new -key server.key -out server.csr
    # 使用私钥和 CSR 生成自签名证书
    openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

    你会需要回答一些问题,比如国家、组织等,最终你会得到 server.keyserver.crt 两个文件,将它们和你的服务器代码放在同一个目录下。

服务器端代码 (ssl_server.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#define PORT 4433
// 初始化 OpenSSL
void init_openssl() {
    SSL_load_error_strings();
    OpenSSL_add_ssl_algorithms();
}
// 清理 OpenSSL
void cleanup_openssl() {
    EVP_cleanup();
}
// 创建 SSL 上下文并配置证书
SSL_CTX *create_context() {
    const SSL_METHOD *method;
    SSL_CTX *ctx;
    // 使用 TLS 服务器方法
    method = TLS_server_method();
    // 创建 SSL 上下文
    ctx = SSL_CTX_new(method);
    if (!ctx) {
        perror("Unable to create SSL context");
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }
    // 加载证书
    if (SSL_CTX_use_certificate_file(ctx, "server.crt", SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }
    // 加载私钥
    if (SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }
    // 验证私钥是否正确
    if (!SSL_CTX_check_private_key(ctx)) {
        fprintf(stderr, "Private key does not match the public certificate\n");
        exit(EXIT_FAILURE);
    }
    return ctx;
}
// 处理客户端连接
void handle_connection(SSL *ssl, int client_sock) {
    char buffer[1024] = {0};
    int bytes;
    // 接收客户端数据
    bytes = SSL_read(ssl, buffer, sizeof(buffer) - 1);
    if (bytes > 0) {
        buffer[bytes] = '\0';
        printf("Client says: %s\n", buffer);
    } else {
        ERR_print_errors_fp(stderr);
    }
    // 发送响应数据
    char *response = "Hello from SSL Server!";
    SSL_write(ssl, response, strlen(response));
    // 关闭 SSL 连接
    SSL_shutdown(ssl);
    SSL_free(ssl);
    close(client_sock);
}
int main() {
    int server_sock, client_sock;
    struct sockaddr_in serv_addr, cli_addr;
    SSL_CTX *ctx;
    // 1. 初始化 OpenSSL
    init_openssl();
    // 2. 创建 SSL 上下文
    ctx = create_context();
    // 3. 创建 TCP Socket
    server_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sock < 0) {
        perror("Unable to create socket");
        exit(EXIT_FAILURE);
    }
    // 设置 SO_REUSEADDR 选项,避免地址占用问题
    int opt = 1;
    if (setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    // 绑定地址和端口
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(PORT);
    if (bind(server_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("Bind failed");
        exit(EXIT_FAILURE);
    }
    printf("Server bound to port %d\n", PORT);
    // 监听连接
    if (listen(server_sock, 10) < 0) {
        perror("Listen failed");
        exit(EXIT_FAILURE);
    }
    printf("Server is listening...\n");
    // 主循环,接受客户端连接
    while (1) {
        socklen_t cli_len = sizeof(cli_addr);
        client_sock = accept(server_sock, (struct sockaddr *)&cli_addr, &cli_len);
        if (client_sock < 0) {
            perror("Accept failed");
            continue;
        }
        printf("New connection from %s:%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
        // 4. 为新连接创建 SSL 结构体
        SSL *ssl = SSL_new(ctx);
        SSL_set_fd(ssl, client_sock);
        // 5. 建立 SSL 连接 (执行 TLS 握手)
        if (SSL_accept(ssl) <= 0) {
            ERR_print_errors_fp(stderr);
        } else {
            printf("SSL connection established with client.\n");
        }
        // 6. 处理加密通信
        handle_connection(ssl, client_sock);
    }
    // 清理 (这部分代码通常在服务器关闭时执行)
    SSL_CTX_free(ctx);
    close(server_sock);
    cleanup_openssl();
    return 0;
}

编译和运行

你需要使用 gcc 来编译这些 C 文件,并链接 OpenSSL 库。

编译命令:

# 编译服务器
gcc -o ssl_server ssl_server.c -lssl -lcrypto
# 编译客户端
gcc -o ssl_client ssl_client.c -lssl -lcrypto

运行步骤:

  1. 确保你生成了 server.crtserver.key 文件,并且它们与 ssl_server.c 在同一目录。

  2. 首先在终端运行服务器:

    ./ssl_server

    你会看到 "Server is listening..." 的输出。

  3. 然后在另一个终端运行客户端:

    ./ssl_client
  4. 预期输出:

    • 服务器端终端:

      Server bound to port 4433
      Server is listening...
      New connection from 127.0.0.1:54321  (端口号可能不同)
      SSL connection established with client.
      Client says: Hello from SSL Client!
    • 客户端终端:

      Connected to server.
      SSL connection established.
      Server says: Hello from SSL Server!

常见问题和注意事项

  1. -lssl -lcrypto 的顺序很重要 在链接时,通常需要 -lcrypto-lssl 之前,这是因为 libssl 库依赖于 libcrypto 库。

  2. 错误处理 OpenSSL 的函数在出错时通常会返回 0NULL,你应该调用 ERR_print_errors_fp(stderr); 来打印详细的错误信息到标准错误输出,这对于调试至关重要。

  3. 证书验证 (客户端) 上面的客户端代码没有验证服务器的证书,在生产环境中,这是不安全的,你应该让客户端加载一个受信任的 CA 证书列表,然后使用 SSL_get_verify_result()SSL_CTX_set_verify() 来验证服务器证书的有效性(比如是否由可信的 CA 签发、是否过期、域名是否匹配等)。

  4. 内存泄漏 SSL_new, SSL_CTX_new 等函数会分配内存,请务必确保在程序结束时调用对应的 SSL_free, SSL_CTX_free 等函数来释放资源,否则会导致内存泄漏。

  5. 现代 TLS 版本 示例代码中使用了 TLS_server_method()TLS_client_method(),这是推荐的做法,因为它们会自动协商使用客户端和服务器都支持的、最新的安全 TLS 版本,避免使用过时的 SSLv23_method 或特定版本如 TLSv1_2_method,除非你有特殊需求。

-- 展开阅读全文 --
头像
C语言double除法结果为何会失真?
« 上一篇 今天
dede自己写采集工具
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

目录[+]