文章目录
- 背景
- 常用加密方式
- SSL
- OpenSSL
- 主要功能
- 库结构
- 交互流程
- 证书生成
- 生成 RSA 私钥
- 私钥的主要组成部分
- 私钥的格式
- 创建自签名证书:
- 签发证书
- 服务器端代码
- 客户端代码
- 常见错误
- 版本问题
- 证书问题
- 证书格式
背景
网络传输中为保证数据安全,通常需要加密
常用加密方式
- 对称加密:
特点:使用同一个密钥进行加密和解密,速度快,但密钥管理复杂,适合在密钥交换后的大量数据传输。
常用算法:DES、3DES、AES等。 - 非对称加密:
特点:使用一对密钥(公钥和私钥),公钥用于加密,私钥用于解密,或者私钥用于签名,公钥用于验证。这种方式解决了密钥交换的问题,但加密解密速度较慢。
常用算法:RSA、DSS、ECC等。 - 单向加密(散列加密、签名):
特点:只能加密,不能解密,常用于生成数据的摘要或验证数据的完整性。元数据改变签名大概率发生变化
常用算法:MD5、SHA-1、SHA-256、SHA-512等。
SSL
SSL(Secure Sockets
Layer)是最常用的网络传输加密协议之一,但需要注意的是,随着技术的发展,SSL的继任者TLS(Transport Layer
Security)已经成为更广泛使用的标准。然而,由于历史原因,“SSL”这一术语在日常使用中仍然非常普遍。
SSL/TLS协议通过结合对称加密、非对称加密和数字签名等多种加密技术,为网络通信提供了强大的安全保障。RSA、DSA和ECC等算法在SSL/TLS中扮演着重要的角色,分别用于密钥交换、数字签名等关键过程。
SSL/TLS协议主要包括以下几个关键组成部分:
- 加密技术:SSL/TLS利用对称加密和非对称加密技术来保护数据的机密性和完整性。
- 对称加密(如AES)用于加密和解密实际传输的数据,因为它速度快且效率高。
- 非对称加密(如RSA、DSS、ECC)则用于密钥交换和数字签名,以确保通信双方的身份验证和数据的不可抵赖性。
- 密钥交换:在SSL/TLS握手过程中,客户端和服务器会协商一个共同的加密密钥(会话密钥),用于后续的数据加密和解密。这个过程中,非对称加密算法(如RSA、ECC)被用来安全地交换密钥信息。
- 数字签名:数字签名用于验证通信双方的身份和数据的完整性。在SSL/TLS中,服务器会向客户端发送一个包含其公钥和身份信息的证书,该证书由受信任的证书颁发机构(CA)签发,并使用非对称加密算法(如RSA、ECC)进行签名。客户端通过验证证书的签名来确认服务器的身份。
关于RSA、DSS(通常指DSA,即Digital Signature Algorithm)和ECC(Elliptic Curve Cryptography)与SSL/TLS的关系:
- RSA:是一种非对称加密算法,广泛用于SSL/TLS中的密钥交换和数字签名。RSA算法的安全性基于大数分解的困难性,它使用一对密钥(公钥和私钥)来进行加密和解密操作。
- DSA:是一种专门用于数字签名的算法,与RSA类似,也是基于非对称加密原理。DSA在SSL/TLS中主要用于验证证书的数字签名,而不是用于密钥交换。
- ECC:是一种基于椭圆曲线密码学的加密算法,与RSA相比,ECC提供了更高的安全性和更短的密钥长度。ECC在SSL/TLS中的应用越来越广泛,因为它能够在保持相同安全性的同时减少计算量和存储需求。
OpenSSL
OpenSSL 是一个强大的开源加密软件库,它提供了多种加密算法、随机数生成器、安全套接字层(SSLv3/TLS)协议实现等功能。OpenSSL
不仅可以用于开发安全的网络应用程序,还可以作为一个命令行工具来处理加密相关的任务。
主要功能
- 加密算法: 包括对称加密算法(如 AES)、非对称加密算法(如 RSA 和 ECC)、哈希算法(如 SHA-256)等。
- 随机数生成: 提供了安全的随机数生成器。
- 安全套接字层 (SSL/TLS): 实现了 SSLv3 和 TLS 协议,用于加密网络通信。
- X.509 证书管理: 支持 X.509 标准的数字证书的创建、验证和管理。
- PKCS 标准: 支持 PKCS#7、PKCS#12 等标准。
库结构
- Crypto Library: 提供基本的加密功能。
- SSL Library: 基于 Crypto Library 提供 SSL/TLS 协议的支持。
命令行工具:
openssl: 提供了丰富的命令行选项,用于执行各种加密操作
版本查看
openssl version -a
交互流程
证书生成
生成 RSA 私钥
openssl genrsa -out server.key 2048
OpenSSL
生成的私钥的数据结构并不是简单的随机数集合,而是遵循特定加密算法和协议规定的复杂数据结构。私钥的格式和内容取决于所使用的加密算法(如RSA、DSA、ECDSA等)和密钥的格式(如PEM、DER等)。
私钥的主要组成部分
对于不同的加密算法,私钥的组成会有所不同,但通常包括以下几个基本部分:
- 算法标识:指明使用的是哪种加密算法(如RSA、DSA、ECDSA等)。
- 参数:包括加密算法所需的具体参数。例如,对于RSA算法,这些参数可能包括两个大的质数(p和q)、模数(n = p * q)、公钥和私钥指数(e和d)等。
- 随机性:私钥的生成通常涉及一定程度的随机性,以确保私钥的不可预测性和安全性。然而,这并不意味着私钥完全由随机数组成,而是随机数用于生成私钥的某些关键部分(如私钥指数d)。
私钥的格式
私钥可以以不同的格式存储,最常见的两种是PEM(Privacy Enhanced Mail)和DER(Distinguished Encoding Rules):
- PEM格式:以-----BEGIN PRIVATE KEY-----和-----END PRIVATE KEY-----为边界,内部是Base64编码的私钥数据。PEM格式是文本格式,易于阅读和存储,但可能会包含额外的换行符和空格。
- DER格式:DER是二进制格式,直接存储私钥的二进制数据,没有额外的编码或标记。DER格式更加紧凑,适用于需要最小化大小的场景,如嵌入式系统。
创建自签名证书:
openssl req -new -key server.key -out server.csr
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
在创建证书的过程中,你可能需要输入一些信息,
如国家代码、组织名称等。你可以填写适当的值或直接按回车键使用默认值
使用openssl req -new -x509 -key server.key -out server.crt -days 365
-nodes命令生成的证书(server.crt)是一个自签名的X.509证书,其数据结构包含了多个关键的信息字段。以下是该证书数据结构中通常包含的内容:
- 证书版本(Version):
表明证书遵循的X.509版本,常见的版本有V1、V2和V3。 - 序列号(Serial Number):
由证书颁发机构(CA)分配的唯一数字,用于唯一标识该证书。 - 签名算法(Signature Algorithm):
指出用于签名证书的算法,如SHA-256 with RSA等。 - 颁发者(Issuer):
指出颁发该证书的实体,由于是自签名证书,颁发者和主体(Subject)通常相同。 - 有效期(Validity Period):
包括证书的生效日期(Not Before)和失效日期(Not After),本例中有效期为365天。 - 主体(Subject):
包含证书持有者的信息,如组织名(O)、组织单位名(OU)、通用名(CN,即- Common Name,通常指域名或服务器名)等。这些信息在openssl req命令执行时会通过交互式输入或配置文件提供。 - 公钥信息(Public Key Info):
包含证书的公钥和公钥使用的算法(如RSA)。公钥用于与私钥配对,进行加密/解密和签名/验证操作。 - 证书签名(Signature):
使用颁发者的私钥对证书内容的哈希值进行签名,确保证书内容的完整性和真实性。由于是自签名证书,这里的签名是使用server.key中的私钥生成的。 - 其他可选字段:
如主题备用名称(Subject Alternative Name,SANs),可以包含除了CN之外的多个域名或IP地址,增强证书的灵活性。 - 扩展(Extensions):
X.509 V3证书支持扩展字段,用于包含额外的信息,如证书吊销列表(CRL)的分发点、密钥用途(Key Usage)、扩展密钥用途(Extended Key Usage)等。
签发证书
上述例子由于是自签名证书,其颁发者信息、签名和公钥都是基于server.key中的私钥生成的,因此不需要外部证书颁发机构(CA)的参与。自签名证书通常用于测试和开发环境,而不推荐在生产环境中使用,因为自签名证书无法由客户端自动验证其真实性,可能会导致安全警告。
线上环境需要购买证书 比如使用XX云
服务器端代码
版本 OpenSSL 1.0.2
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#define PORT 8443
#define BUFFER_SIZE 1024
void handle_ssl_connection(SSL *ssl) {
char buffer[BUFFER_SIZE];
char resp[BUFFER_SIZE+10];
int bytes_received;
while ((bytes_received = SSL_read(ssl, buffer, BUFFER_SIZE)) > 0) {
buffer[bytes_received] = '\0';
printf("Received: %s\n", buffer);
sprintf(resp,"srv recv %s",buffer);
// 响应客户端
SSL_write(ssl,resp, strlen(resp) + 1);
}
if (bytes_received <= 0) {
fprintf(stderr, "Error on read: %d\n", bytes_received);
}
}
int main() {
int listen_fd, conn_fd;
struct sockaddr_in serv_addr;
SSL_CTX *ctx;
SSL *ssl;
int opt = 1;
// 创建 socket
if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
// 设置 SO_REUSEADDR 选项
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("Setsockopt failed");
exit(EXIT_FAILURE);
}
// 绑定地址
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(PORT);
if (bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("Bind failed");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(listen_fd, 5) < 0) {
perror("Listen failed");
exit(EXIT_FAILURE);
}
// 初始化 SSL 上下文
SSL_library_init();
ctx = SSL_CTX_new(TLSv1_2_method());
if (!ctx) {
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 certificate public key\n");
exit(EXIT_FAILURE);
}
// 接受客户端连接
while (1) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
conn_fd = accept(listen_fd,(struct sockaddr *)&client_addr, &client_len);
if (conn_fd < 0) {
perror("Accept failed");
continue;
}
printf("Accepted client %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
ssl = SSL_new(ctx);
SSL_set_fd(ssl, conn_fd);
// 建立 SSL 连接
if (SSL_accept(ssl) == -1) {
ERR_print_errors_fp(stderr);
SSL_free(ssl);
close(conn_fd);
continue;
}
// 处理 SSL 连接
handle_ssl_connection(ssl);
// 清理资源
SSL_shutdown(ssl);
SSL_free(ssl);
close(conn_fd);
}
SSL_CTX_free(ctx);
close(listen_fd);
return 0;
}
客户端代码
#include <stdio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define RCVBUFSIZE 1024
/*
gcc -g -o client client.c -lssl -lcrypto
*/
int main() {
int sock;
struct sockaddr_in serv_addr;
char user_input[1024];
char buffer[RCVBUFSIZE];
int recv_len =0;
SSL_CTX *ctx;
SSL *ssl;
int res = 0;
int i = 0;
SSL_library_init();
ctx = SSL_CTX_new(TLSv1_2_method());
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8443);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
printf("socket connect failed\n");
}
printf("connect client, socket:%d\n", sock);
ssl = SSL_new(ctx);
if (ssl == NULL) {
printf("socket:%d, SSL_new failed\n", sock);
goto sslerror;
}
SSL_set_fd(ssl, sock);
if (SSL_connect(ssl) < 0) {
printf("socket:%d, SSL_accept failed\n", sock);
goto sslerror;
}
printf("ssl connected ok\n");
for(i+0;i<500;i++){ // test
printf("Enter data to send: ");
fgets(user_input,sizeof(user_input)-1, stdin);
int result = SSL_write(ssl, user_input, strlen(user_input));
if (result <= 0) {
int error = SSL_get_error(ssl, result);
switch (error) {
case SSL_ERROR_ZERO_RETURN:
printf("SSL_write error: Connection closed\n");
break;
case SSL_ERROR_WANT_READ:
case SSL_ERROR_WANT_WRITE:
printf("SSL_write error: Try again later\n");
break;
case SSL_ERROR_SYSCALL:
perror("SSL_write error");
break;
default:
printf("SSL_write error: %d\n", error);
break;
}
}
recv_len = SSL_read(ssl, buffer, sizeof(buffer));
printf("Received[%d] %s\n",recv_len,buffer);
}
sslerror:
SSL_shutdown(ssl);
close(sock);
if (ssl) SSL_free(ssl);
return 0;
}
常见错误
版本问题
新旧版本接口API会有不同
证书问题
139851570730928:error:140A90A1:lib(20):func(169):reason(161):ssl_lib.c:1966
- lib(20) 表示错误发生在 SSL 库中。
- func(169) 表示错误发生在 SSL 库中的某个特定函数。
- reason(161) 表示错误的具体原因,通常表示私钥与证书不匹配或私钥格式不正确。
- ssl_lib.c:1966 表示错误发生的源代码位置
解决方法 - 确认私钥格式:
确保私钥文件 server.key 的格式是 PEM 格式。
使用 openssl rsa -in server.key -check 来检查私钥文件是否正确。 - 确认证书和私钥匹配:
确保证书和私钥是由同一组密钥对生成的。
使用 openssl x509 -pubkey -in server.crt -noout -text 获取证书中的公钥。
使用 openssl rsa -in server.key -pubout -outform PEM 获取私钥对应的公钥,并与证书中的公钥进行比较。 - 重新生成证书和私钥:
如果私钥文件格式不正确或与证书不匹配,可以尝试重新生成证书和私钥文件。
使用 OpenSSL 命令行工具重新生成文件。 - 检查文件路径:
确认 server.crt 和 server.key 文件的路径是正确的。
如果文件不在当前目录下,确保提供完整的路径。 - 设置正确的文件权限:
确保您的程序有足够的权限读取证书和私钥文件。
可以尝试更改文件权限以允许程序访问:chmod 600 server.crt server.key
证书格式
SSL_FILETYPE_PEM 是 OpenSSL 中用于处理 PEM (Privacy Enhanced Mail) 格式的常量。PEM
是一种广泛使用的 ASCII 编码格式,用于存储证书、密钥和其他加密数据。除了 PEM 格式之外,OpenSSL 还支持其他几种格式,例如
DER (Distinguished Encoding Rules) 格式。
PEM 格式的特点是使用 Base64 编码,并以特定的文本标记开始和结束。例如,一个 PEM 格式的私钥文件通常看起来像这样:
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAwUq...
...
-----END RSA PRIVATE KEY-----
PEM 格式的证书文件通常看起来像这样:
-----BEGIN CERTIFICATE-----
MIIEowIBAAKCAQEAwUq...
...
-----END CERTIFICATE-----
DER 格式
DER 格式是一种二进制编码格式,它不包含任何文本标记。这种格式比 PEM 更紧凑,但不是文本可读的。一个 DER
格式的文件通常是一个纯二进制文件,没有额外的文本标记。
在 OpenSSL 中使用不同格式
- 使用 PEM 格式:
使用 SSL_FILETYPE_PEM 常量来加载 PEM 格式的证书和密钥文件。 - 使用 DER 格式:
使用 SSL_FILETYPE_ASN1 常量来加载 DER 格式的证书和密钥文件。