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

99ANYc3cd6
预计阅读时长 32 分钟
位置: 首页 C语言 正文

C 语言本身没有内置的 SSL/TLS 支持,SSL/TLS 是一个复杂的加密协议栈,需要借助第三方库来实现,最常用、最权威的库是 OpenSSL

c语言 sslsocket
(图片来源网络,侵删)

当我们在 C 语言中谈论 "SSL Socket" 时,我们通常指的是:使用 OpenSSL 库,在标准的 TCP Socket 之上,构建一个安全的、加密的通信通道

这个过程可以形象地理解为:

  1. 建立地基:使用标准的 socket(), bind(), listen(), accept() (服务器端) 或 socket(), connect() (客户端) 函数创建和连接一个普通的 TCP Socket。
  2. 安装安全门锁:在 TCP 连接建立后,使用 OpenSSL 的函数对这个 Socket 进行“包装”或“升级”,将其从一个明文的 Socket 变成一个加密的 SSL Socket。
  3. 安全通信:之后,你不再使用 send()/recv(),而是使用 OpenSSL 提供的 SSL_write()/SSL_read() 函数进行数据收发,所有数据都会在发送前自动加密,在接收后自动解密。

核心组件

在开始编程之前,需要了解 OpenSSL 中几个核心的数据结构:

  1. SSL_CTX (SSL Context): SSL 上下文,你可以把它看作是 SSL/TLS 连接的“工厂”或“配置中心”,它包含了 SSL/TLS 的全局配置,比如使用的协议版本(TLS 1.2, 1.3)、加密套件、证书、私钥等,创建一个 SSL 对象时,通常需要一个 SSL_CTX 对象。
  2. SSL: 代表一个具体的 SSL/TLS 连接,每个客户端连接到服务器时,都会创建一个独立的 SSL 对象,它包含了该连接的状态信息,比如当前加密会话、密钥等,所有的读写操作都是通过这个 SSL 对象来完成的。
  3. BIO (Basic Input/Output): 这是 OpenSSL 的 I/O 抽象层,它是一个强大的工具,可以将 SSL/TLS 流连接到任何底层的 I/O 通道上,比如文件描述符(Socket)、内存、甚至自定义的回调函数,我们最常用的就是将 SSL 连接到 Socket 的文件描述符上。

编程步骤

下面我们分别介绍 SSL 服务器和客户端的编程流程。

c语言 sslsocket
(图片来源网络,侵删)

准备工作:编译时链接 OpenSSL

在编译你的 C 程序时,你需要链接 OpenSSL 库,通常使用以下命令:

# 对于简单的程序
gcc -o myssl_app myssl_app.c -lssl -lcrypto
# 对于更复杂的程序,可能需要指定路径
gcc -o myssl_app myssl_app.c -I/usr/local/ssl/include -L/usr/local/ssl/lib -lssl -lcrypto
  • -lssl: 链接 OpenSSL 的 SSL 库。
  • -lcrypto: 链接 OpenSSL 的加密算法库(libcrypto),libssl 依赖它。
  • -I-L: OpenSSL 不在系统默认路径下,需要指定其头文件和库文件的路径。

SSL 服务器端实现

服务器端需要持有自己的证书和私钥,以便向客户端证明自己的身份。

步骤:

  1. 初始化 OpenSSL 库
  2. 创建 SSL 上下文 (SSL_CTX)
  3. 加载证书和私钥到上下文中。
  4. 创建 TCP Socket,并绑定、监听。
  5. 进入主循环,接受客户端连接。
  6. 将新的 Socket 连接“包装”成 SSL 连接 (SSL)
  7. 执行 SSL 握手
  8. 通过 SSL 连接进行安全通信 (SSL_write/SSL_read)
  9. 关闭连接和清理资源

示例代码框架:

#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>
// 初始化 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;
    // 使用 TLSv1_2_server_method 或 TLS_server_method
    method = TLS_server_method();
    ctx = SSL_CTX_new(method);
    if (!ctx) {
        perror("Unable to create SSL context");
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }
    return ctx;
}
// 配置上下文,加载证书和私钥
void configure_context(SSL_CTX *ctx) {
    // 加载证书文件
    if (SSL_CTX_use_certificate_file(ctx, "server-cert.pem", SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }
    // 加载私钥文件
    if (SSL_CTX_use_PrivateKey_file(ctx, "server-key.pem", SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }
}
int main() {
    int sock;
    struct sockaddr_in addr;
    init_openssl();
    SSL_CTX *ctx = create_context();
    configure_context(ctx);
    // 1. 创建 TCP Socket
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("Unable to create socket");
        exit(EXIT_FAILURE);
    }
    addr.sin_family = AF_INET;
    addr.sin_port = htons(4433);
    addr.sin_addr.s_addr = INADDR_ANY;
    // 2. 绑定和监听
    if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("Unable to bind");
        exit(EXIT_FAILURE);
    }
    if (listen(sock, 1) < 0) {
        perror("Unable to listen");
        exit(EXIT_FAILURE);
    }
    // 3. 接受客户端连接
    printf("Server is listening on port 4433...\n");
    int client = accept(sock, (struct sockaddr*)&addr, (socklen_t*)&sizeof(addr));
    if (client < 0) {
        perror("Unable to accept");
        exit(EXIT_FAILURE);
    }
    // 4. 将 Socket 包装成 SSL 连接
    SSL *ssl = SSL_new(ctx);
    SSL_set_fd(ssl, client);
    // 5. 执行 SSL 握手
    if (SSL_accept(ssl) <= 0) {
        ERR_print_errors_fp(stderr);
    } else {
        char reply[1024] = "Hello, this is a secure SSL server!";
        // 6. 通过 SSL 安全通信
        SSL_write(ssl, reply, strlen(reply));
    }
    // 7. 关闭连接
    SSL_shutdown(ssl);
    SSL_free(ssl);
    close(client);
    close(sock);
    // 8. 清理
    SSL_CTX_free(ctx);
    cleanup_openssl();
    return 0;
}

SSL 客户端实现

客户端需要验证服务器的证书(可选,但强烈推荐)。

步骤:

  1. 初始化 OpenSSL 库
  2. 创建 SSL 上下文 (SSL_CTX)
  3. (可选但推荐) 加载 CA 证书,用于验证服务器证书。
  4. 创建 TCP Socket
  5. 连接到服务器
  6. 将 Socket 连接“包装”成 SSL 连接 (SSL)
  7. 执行 SSL 握手
  8. 通过 SSL 连接进行安全通信 (SSL_write/SSL_read)
  9. 关闭连接和清理资源

示例代码框架:

#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>
// ... (init_openssl 和 cleanup_openssl 函数与服务器端相同) ...
// 创建 SSL 上下文
SSL_CTX *create_context() {
    const SSL_METHOD *method;
    SSL_CTX *ctx;
    method = TLS_client_method();
    ctx = SSL_CTX_new(method);
    if (!ctx) {
        perror("Unable to create SSL context");
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }
    return ctx;
}
// 配置客户端上下文,加载 CA 证书用于验证
void configure_context(SSL_CTX *ctx) {
    // 加载受信任的 CA 证书
    if (SSL_CTX_load_verify_locations(ctx, "ca-cert.pem", NULL) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }
    // 设置验证深度和是否必须验证
    SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
    SSL_CTX_set_verify_depth(ctx, 1);
}
int main() {
    int sock;
    struct sockaddr_in addr;
    SSL *ssl;
    SSL_CTX *ctx;
    init_openssl();
    ctx = create_context();
    configure_context(ctx);
    // 1. 创建 TCP Socket
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("Unable to create socket");
        exit(EXIT_FAILURE);
    }
    addr.sin_family = AF_INET;
    addr.sin_port = htons(4433);
    // 替换为你的服务器 IP
    if (inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) <= 0) {
        perror("Invalid address/ Address not supported");
        exit(EXIT_FAILURE);
    }
    // 2. 连接服务器
    if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("Unable to connect");
        exit(EXIT_FAILURE);
    }
    // 3. 将 Socket 包装成 SSL 连接
    ssl = SSL_new(ctx);
    SSL_set_fd(ssl, sock);
    // 4. 执行 SSL 握手
    if (SSL_connect(ssl) <= 0) {
        ERR_print_errors_fp(stderr);
    } else {
        char buf[1024] = {0};
        // 5. 通过 SSL 安全通信
        SSL_read(ssl, buf, sizeof(buf));
        printf("Server says: %s\n", buf);
    }
    // 6. 关闭连接
    SSL_shutdown(ssl);
    SSL_free(ssl);
    close(sock);
    // 7. 清理
    SSL_CTX_free(ctx);
    cleanup_openssl();
    return 0;
}

函数类别 服务器端 客户端 描述
上下文 SSL_CTX_new(TLS_server_method()) SSL_CTX_new(TLS_client_method()) 创建 SSL 上下文
SSL_CTX_use_certificate_file() SSL_CTX_load_verify_locations() 加载证书/CA 证书
SSL_CTX_use_PrivateKey_file() 加载私钥
连接 SSL_new() SSL_new() 从上下文创建一个 SSL 连接对象
SSL_set_fd(ssl, socket_fd) SSL_set_fd(ssl, socket_fd) 将 SSL 连接绑定到 Socket 文件描述符
SSL_accept(ssl) SSL_connect(ssl) 执行 SSL/TLS 握手
通信 SSL_write(ssl, data, len) SSL_write(ssl, data, len) 加密发送数据
SSL_read(ssl, buf, len) SSL_read(ssl, buf, len) 接收并解密数据
关闭 SSL_shutdown(ssl) SSL_shutdown(ssl) 通知对方关闭 SSL 连接
SSL_free(ssl) SSL_free(ssl) 释放 SSL 连接对象
SSL_CTX_free(ctx) SSL_CTX_free(ctx) 释放 SSL 上下文

获取证书和私钥

要运行上面的示例,你需要生成一些测试用的证书文件,你可以使用 OpenSSL 的命令行工具来完成:

c语言 sslsocket
(图片来源网络,侵删)
  1. 生成 CA 证书 (用于签名服务器证书)

    openssl genrsa -out ca-key.pem 2048
    openssl req -new -x509 -days 365 -key ca-key.pem -out ca-cert.pem -subj "/CN=My Test CA"
  2. 生成服务器私钥和证书签名请求 (CSR)

    openssl genrsa -out server-key.pem 2048
    openssl req -new -key server-key.pem -out server-csr.pem -subj "/CN=localhost"
  3. 使用 CA 签名服务器 CSR,生成服务器证书

    openssl x509 -req -days 365 -in server-csr.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem

你将拥有 ca-cert.pem, server-key.pem, 和 server-cert.pem 这三个文件,可以用于上面的示例程序了。

高级主题与注意事项

  • 错误处理:OpenSSL 的错误处理通过 ERR_get_error() 等函数获取错误堆栈,并使用 ERR_print_errors_fp() 打印到文件,务必对每个可能失败的 OpenSSL 函数调用进行错误检查。
  • 非阻塞 I/O:在高性能服务器中,Socket 和 SSL I/O 通常设置为非阻塞模式,配合 select(), poll(), 或 epoll 来实现事件驱动。
  • SNI (Server Name Indication):如果一台服务器需要提供多个域名的 HTTPS 服务,客户端需要在握手时告诉它自己想访问哪个域名,这可以通过 SSL_set_tlsext_host_name() 函数实现。
  • 性能:SSL/TLS 握手是计算密集型操作,开销较大,对于高并发场景,通常会使用 会话恢复会话缓存 来避免重复握手,提高性能。

希望这份详细的指南能帮助你理解和使用 C 语言中的 SSL Socket 编程!

-- 展开阅读全文 --
头像
dede模板首页怎么做?
« 上一篇 02-28
织梦如何正确调用二级导航菜单?
下一篇 » 02-28

相关文章

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

目录[+]