0%

openssl学习记录

一、前言

1
2
3
PS D:\work\src\local\cpp\openssl\1.1.1> .\apps\openssl.exe
OpenSSL> version
OpenSSL 1.1.1k 25 Mar 2021

1. 下载

2. 编译

2.1. 二进制编译

1) linux

1
2
3
4
5
6
# --debug 编译带符号
=> ./config --debug --prefix=/path/to/install
# 编译
=> make -j8
# 安装
=> make install

2.2. 引擎编译

二、命令

1. 一些基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
# 枚举支持的加密套件
=> openssl ciphers -V | column -t
0x13,0x02 - TLS_AES_256_GCM_SHA384 TLSv1.3 Kx=any Au=any Enc=AESGCM(256) Mac=AEAD
0x13,0x03 - TLS_CHACHA20_POLY1305_SHA256 TLSv1.3 Kx=any Au=any Enc=CHACHA20/POLY1305(256) Mac=AEAD
0x13,0x01 - TLS_AES_128_GCM_SHA256 TLSv1.3 Kx=any Au=any Enc=AESGCM(128) Mac=AEAD
0xC0,0x2C - ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(256) Mac=AEAD
0xC0,0x30 - ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(256) Mac=AEAD
0x00,0x9F - DHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=DH Au=RSA Enc=AESGCM(256) Mac=AEAD
0xCC,0xA9 - ECDHE-ECDSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=ECDSA Enc=CHACHA20/POLY1305(256) Mac=AEAD
0xCC,0xA8 - ECDHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=RSA Enc=CHACHA20/POLY1305(256) Mac=AEAD
0xCC,0xAA - DHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=DH Au=RSA Enc=CHACHA20/POLY1305(256) Mac=AEAD
0xC0,0x2B - ECDHE-ECDSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(128) Mac=AEAD
...

1.1. 国密openssl命令

1
2
# 列举支持的椭圆曲线
openssl_gm ecparam -list_curves

2. 证书

1.1. 颁发证书

(1) 国际密码标准(普密)

1. 颁发证书需要先生成一个颁发机构,也就是CA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 新建目录防止混乱
mkdir -p ssl_diy/private && cd ssl_diy
# 生成ca的私钥
openssl genrsa -out private/cakey.pem 2048
# 根据key生成CA的证书文件
openssl req -new -x509 -days 3650 -key private/cakey.pem -out cacert.pem
# 将有一些配置需要填写,可以随意填写,因为是自颁发
# Country Name (2 letter code) [AU]: 国家名,自然CN
# State or Province Name (full name) [Some-State]: 省份名称,比如Hunan
# Locality Name (eg, city) []: 城市名称,比如changsha
# Organization Name (eg, company) [Internet Widgits Pty Ltd]: 公司名称
# Organizational Unit Name (eg, section) []: 部门名称
# Common Name (eg, YOUR name) []: 颁发者的名字
# Email Address []: 邮件地址

# 也可以直接使用-subj指定subject
openssl req -new -x509 -key private/cakey.pem -out cacert.pem -subj "/C=CN/ST=Beijing/L=Haidian/O=Datang/OU=SDT/OU=abc/CN=Shixun/emailAddress=dongzy08@qq.com"

# pem转crt格式
openssl x509 -outform der -in cacert.pem -out cacert.crt

2. 生成证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
######### 生成证书 ###########
# 同样生成私钥key
openssl genrsa -out domain.key 2048
# 根据key生成证书请求文件
openssl req -new -key domain.key -out domain.csr
# 将有一些配置需要填写,可以随意填写,因为是自颁发
# Country Name (2 letter code) [AU]: 国家名,自然CN
# State or Province Name (full name) [Some-State]: 省份名称,比如Hunan
# Locality Name (eg, city) []: 城市名称,比如changsha
# Organization Name (eg, company) [Internet Widgits Pty Ltd]: 公司名称
# Organizational Unit Name (eg, section) []: 部门名称
# Common Name (eg, server FQDN or YOUR name) []: 填写网站的域名
# Email Address []: 邮件地址

# 也可以直接使用-subj指定subject
openssl req -new -key domain.key -out domain.csr -subj "/C=CN/ST=Beijing/L=Haidian/O=Datang/OU=SDT/OU=abc/CN=Shixun/emailAddress=dongzy08@qq.com"

######### 使用ca给证书签名,签了名的证书才是真正的证书 ###########
# 签名前需要有几个准备文件
mkdir -p demoCA/newcerts
touch demoCA/index.txt
echo 01 > demoCA/serial
# 签名,可以自定义有效天数-days
openssl ca -in domain.csr -cert cacert.pem -keyfile private/cakey.pem -days 365 -out domain.crt

# 生成p12,需要设置密码
openssl pkcs12 -export -out domain.p12 -inkey domain.key -in domain.crt

(2) 中国密码标准(国密)

0. 准备工作

1
2
mkdir -p ssl_diy/private
cd ssl_diy

1. 生成用户证书

  1. 根据椭圆曲线生成密钥
1
openssl_gm ecparam -genkey -name SM2 -out private/root-cakey.pem
  • 生成的密钥内容如下
1
2
3
4
5
6
7
8
-----BEGIN EC PARAMETERS-----
BggqgRzPVQGCLQ==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIPS25CUNA9sedtqmVHAkIBhknFUAREy5+eEfg/vkX30noAoGCCqBHM9V
AYItoUQDQgAEnnSjGFJ3LG1qA7wYGpnRxr6palpBtuw4yToNW7QRZKz12s438i1G
Dqxf97ZcfeI6pAxb+e4TOSLu7tIt+wFyng==
-----END EC PRIVATE KEY-----

(3) 生成带拓展字段的证书

  • 需要拷贝一份/etc/ssl/openssl.cnf文件到自己的目录下
  • 修改里面的字段,添加下面的字段
1
2
3
4
5
6
7
8
[ v3_req ]
# 添加下面语句
subjectAltName = @alt_names
...
# 添加下面块,DNS加几个自己决定
[ alt_names ]
DNS.1 = www.test.com
DNS.2 = *.aaa.com
  • 然后在生成证书(非生成CA)的命令中加上-config /path/to/openssl.cnf
  • 签名命令中加上-config /path/to/openssl.cnf -extensions v3_req

1.2. 证书转换

csr(Certificate Signing Request)

  • 通常为证书请求,PEM格式编码,包含证书请求信息和公钥,通常是没有签名的证书

crt/cer(Certificate)

  • 通常为被ca机构签名后的证书格式,包含证书本身的信息(证书和公钥)和证书颁发机构的信息
  • crt一般在linux上使用,cer一般在windows上使用
  • 两种格式都可以使用pem编码或者der编码

pem(Privacy-Enhanced Mail)

  • pem是储存证书和密钥的一种格式,一般是base64编码,以-----BEGIN CERTIFICATE----------END CERTIFICATE-----包裹
1
2
# pem格式转der格式
openssl x509 -outform der -in cert.pem -out cert.der

der(Distinguished Encoding Rules)

  • der是一种二进制格式,通常用于在网络上传输证书。cer(Certificate)是DER编码的X.509证书的扩展名,通常用于Windows操作系统中
  • der不能使用CERTIFICATE REQUEST的pem转换,需要使用CERTIFICATE转换,也就是需要ca签名后的证书
1
2
# der格式转pem格式
openssl x509 -inform der -in cert.der -out cert.pem

p12

  • p12是证书和私钥结合的证书,一般作为设备证书导入使用或者导入为浏览器证书做证书认证使用
1
2
3
4
5
6
7
8
9
########## 解开p12证书 ##########
# 只要证书,需要密码
openssl pkcs12 -clcerts -nokeys -in cert.p12 -out cert.pem
# 只要私钥
openssl pkcs12 -nocerts -nodes -in cert.p12 -out key.pem

########## 生成p12证书 ##########
# 需要私钥+证书生成,需要设置密码
openssl pkcs12 -export -out domain.p12 -inkey domain.key -in domain.crt

1.3. 验证证书

1
2
# 使用ca根证书校验客户端证书
openssl verify -CAfile root.crt client.pem

1.4. 查看证书

  • oid的对照关系可以看node-forge/lib/oids.js
1
2
# 打印证书内容,下面命令传入的是ascii字符版证书
openssl x509 -in cert.pem -noout -text

3. 客户端 openssl s_client

3.1. 选项

  • -engine <name>: 使用引擎,给一些使用硬件加密的代码使用
  • -ssl_client_engine <name>: 使用引擎给客户端需要证书操作时
  • -connect <ip:port>: 连接地址,仅支持ip+端口,不支持url
  • -msg: 显示协议相关的消息体
  • -state: 显示当前ssl的阶段
  • -debug: 额外显示一些输出
  • -tls1_1: 使用tlsv1.1进行通信
  • -cipher <ciphers>: 指定tlsv1.2及以下的套件列表
  • -ciphersuites <ciphers>: 指定tlsv1.3套件列表
  • -key <keyPath>: 如果证书-cert没有带key,需要指定私钥
  • -cert <certPath>: 证书,pem格式
  • -servername <serverName>: 添加tls的extension字段server_name
  • -keylogfile <keyfile>: 将预主密钥保存到文件

3.2. 示例用法

1
2
3
4
5
# -connect 127.0.0.1:7777   连接127.0.0.1:7777
# -cipher RSA 使用RSA密钥交换算法相关的算法套件
# -debug 开启调试日志
# -keylogfile test.log 将预主密钥保存到test.log
openssl s_client -connect 127.0.0.1:7777 -cipher RSA -debug -keylogfile test.log

4. 服务端 openssl s_server

  • -accept <port>: 监听端口
  • -tls1_1: 只使用tlsv1.1
  • -cipher <ciphers>: 指定tlsv1.2及以下的套件列表
  • -ciphersuites <ciphers>: 指定tlsv1.3套件列表
  • -state: 显示当前ssl的阶段
  • -CAfile <caPath>: ca根证书路径,用于校验对端证书,pem格式
  • -key <keyPath>: 如果证书-cert没有带key,需要指定私钥
  • -cert <certPath>: 证书,pem格式
  • -verify int: 开启校验对端证书
  • -Verify int: 强制要求对端有证书,并开启校验
  • -verify_return_error: 默认情况下,上面的而校验失败了只会打印日志后继续,加了此选项才会断开连接
  • -WWW: 当前目录作为根目录进行http服务,最简单的http服务,必须输入全路径才能处理包括index.html

4.2. 示例用法

1
2
3
4
5
6
7
# -accept 7777      监听7777端口
# -tls1_2 使用tlsv1.2及以下加密套件
# -state 显示ssl握手阶段
# -debug 显示debug日志
# -key domain.key 证书私钥
# -cert domain.crt 证书和公钥信息
openssl s_server -accept 7777 -tls1_2 -state -debug -key domain.key -cert domain.crt

三、openssl库

1. 引擎开发指南

1.1. 简介

  • 引擎库是openssl给一些硬件设备提供的一个标准化开发接口,可以通过编写一些硬件接口对接openssl实现ssl过程中使用硬件进行加解密等操作
  • 一般引擎库再windows上放置位置为C:\Program Files (x86)\OpenSSL\lib\engines-1_1\xxx.dll文件,加载时会根据id找同名dll文件
  • 实现上对于企业用户,使用引擎库可以作为ukey来证书认证或国密加密认证等
  • ukey不管是普密还是国密,都是无法导出私钥的,证书的认证流程需要使用私钥对证书进行签名,再发送给服务端进行解析,所以引擎库里面不仅需要导出证书接口,还需要实现证书的签名接口,使用ukey的签名接口进行签名
  • skf转openssl结构体参考仓库: https://github.com/guanzhi/GmSSL

1.2. 基础接口

  • 加载引擎必须要定义的下面几个函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/******************** 这个函数是引擎设置的最基础的函数 ********************/

/* Constants used when creating the ENGINE */
static const char* engine_id = "skf";

/* Prepare the ENGINE structure for registration */
static int skf_bind_helper(ENGINE* e) {
LOG_DEBUG(Tag, "skf_bind_helper");

// set skf rsa engine method, use default method and diy some function
if ((skf_rsa_method = RSA_meth_dup(RSA_get_default_method())) == NULL ||
// 私钥加密函数
RSA_meth_set_priv_enc(skf_rsa_method, skf_rsa_priv_enc) == 0 ||
RSA_meth_set_sign(skf_rsa_method, my_sign) == 0 ||
RSA_meth_set_verify(skf_rsa_method, my_verify) == 0) {
return 0;
}

if (!ENGINE_set_id(e, engine_id) || // 引擎id
!ENGINE_set_name(e, engine_name) || // 引擎名字
!ENGINE_set_init_function(e, engine_init) || // 初始化函数,握手前调用
!ENGINE_set_finish_function(e, engine_finish) || // 结束函数,连接完成不再需要引擎时调用
!ENGINE_set_destroy_function(e, engine_destroy) || // 销毁函数,结束时调用,只会被调用一次
!ENGINE_set_RSA(e, skf_rsa_method) || // 设置rsa算法结构体
!ENGINE_set_EC(e, &skf_ecc_meth) || // 设置rsa算法结构体
// if not set, sdf will load by default if possible
// !ENGINE_set_flags(e, ENGINE_FLAGS_NO_REGISTER_ALL) ||
!ENGINE_set_load_ssl_client_cert_function(e, skf_load_ssl_client_cert) // 设置客户端证书加载函数
) {
return 0;
}

return 1;
}

/****************** 下面是给openssl库的标准接口,必须定义的 ********************/

/* This stuff is needed if this ENGINE is being compiled into a self-contained
* shared-library. */
#ifndef OPENSSL_NO_DYNAMIC_ENGINE
#include <openssl/engine.h>
static int bind_helper(ENGINE* e, const char* id) {
if (id && (strcmp(id, engine_id) != 0)) return 0;

if (!skf_bind_helper(e)) return 0;

return 1;
}

IMPLEMENT_DYNAMIC_CHECK_FN()
IMPLEMENT_DYNAMIC_BIND_FN(bind_helper)
#else
static ENGINE* engine_skf(void) {
ENGINE* eng = ENGINE_new();
if (!eng) return NULL;
if (!skf_bind_helper(eng)) {
ENGINE_free(eng);
return NULL;
}

return eng;
}

void engine_load_skf_int(void) {
ENGINE* toadd = engine_skf();
if (!toadd) return;
ENGINE_add(toadd);
ENGINE_free(toadd);
ERR_clear_error();
}
#endif /* OPENSSL_NO_DYNAMIC_ENGINE */
  • 编译出来的引擎库名字和设置中的engine_id要一致

1.3. 编译

  • 在openssl的源码根目录下新建一个文件夹放引擎代码,就假设是my-engine
1
2
3
4
5
6
7
my-engine
├── CMakeLists.txt
├── ecc.cpp
├── ecc.h
├── e_skf.cpp
├── log.hpp
└── Makefile

makefile

  • makefile暂时只支持linux的gcc编译
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
CC = gcc
CXX = g++
CFLAGS = -g -fPIC -Wall -Werror -Wshadow -Wno-unused-function -std=c11
CXXFLAGS = -g -fPIC -Wall -Wshadow -Wno-unused-function -std=c++14
LFLAGS = -shared -fPIC

#要编译的目标
TARGET = skf.so
#源文件路径,跟makefile文件同目录不用填,目录使用/结束
SRCDIR = ./
#编译中间文件路径,跟SRCDIR目录不用填,目录使用/结束
OBJDIR =
#链接库路径,带上-L,-L../
LIB_DIRS = -L../
#链接库名,带上-l,-lsvpn 静态库填写绝对路径
LIB = -lssl -lcrypto
#引用头文件路径,带上-I, -I$(PREFIX_INC)
INCLUDE = -I../include

#如果为空,就是当前目录
ifeq ($(SRCDIR),)
SRCDIR += ./
endif

#下面四个命令通过模式匹配获取当前目录下的所有C,CPP,O文件
CPP_SOURCES = $(foreach d,$(SRCDIR),$(wildcard $(d)*.cpp))
C_SOURCES = $(foreach d,$(SRCDIR),$(wildcard $(d)*.c))
CPP_OBJS = $(patsubst %.cpp, $(OBJDIR)%.o, $(CPP_SOURCES))
C_OBJS = $(patsubst %.c, $(OBJDIR)%.o, $(C_SOURCES))

#下面把一些其他目录下的源文件也放到里面
SOURCES += $(CPP_SOURCES) $(C_SOURCES) $(OBJS:%.o=%.c*)
OBJS += $(CPP_OBJS) $(C_OBJS)

.PHONY: clean all cp cpp cpso _clean _all _install test

.SUFFIXES: .c .cpp .o

.c.o:
$(CC) -c $*.c -o $*.o $(INCLUDE) $(CFLAGS)
.cpp.o:
$(CXX) -c $*.cpp -o $*.o $(INCLUDE) $(CXXFLAGS)


all: $(OUTDIR)$(TARGET) $(_all)

$(OUTDIR)$(TARGET): $(OBJS)
ifeq ($(CPP_OBJS),)
$(CC) $(LFLAGS) -o $(OUTDIR)$(TARGET) $(OBJS) $(LIB_DIRS) $(LIB)
else
$(CXX) $(LFLAGS) -o $(OUTDIR)$(TARGET) $(OBJS) $(LIB_DIRS) $(LIB)
endif

clean: $(_clean)
rm -f $(OUTDIR)$(TARGET) $(OBJS)

cmake

  • 编写CMakeLists.txt,在windows上使用msvc编译的openssl主程序就需要使用msvc编译引擎
  • linux就使用gcc即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# cmake最小支持版本3.8
CMAKE_MINIMUM_REQUIRED(VERSION 3.8)

# 项目工程名字
PROJECT(skf
LANGUAGES C CXX
VERSION 1.0.0.0
DESCRIPTION "skf engine"
)

# 编译命令导出json给其他地方使用
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# 设置C++标准
set(CMAKE_CXX_STANDARD 14)

SET(SRC_FILES)
FILE(GLOB SRC_FILES ./*.c*)

add_library(${PROJECT_NAME} SHARED ${SRC_FILES})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")

target_include_directories(${PROJECT_NAME} PRIVATE
../include
)

target_link_directories(${PROJECT_NAME} PRIVATE
../
)

if (MSVC)
target_compile_options(${PROJECT_NAME} PRIVATE
/wd4819 # 对msvc关闭uincode的warning提示
/wd4996 # 对msvc关闭unsafe func的提示
)
target_link_libraries(${PROJECT_NAME} PRIVATE
libssl
libcrypto
)
else()
target_link_libraries(${PROJECT_NAME} PRIVATE
ssl crypto
)
endif()

1.4. 加载引擎

libcurl调用

  • curl引擎设置如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
size_t curlWriteFunction(void *ptr, size_t size /*always==1*/, size_t nmemb,
void *userdata) {
char **stringToWrite = (char **)userdata;
const char *input = (const char *)ptr;
if (nmemb == 0) return 0;
if (!*stringToWrite)
*stringToWrite = (char *)malloc(nmemb + 1);
else
*stringToWrite = (char *)realloc(*stringToWrite, dataSize + nmemb + 1);
memcpy(*stringToWrite + dataSize, input, nmemb);
dataSize += nmemb;
(*stringToWrite)[dataSize] = '\0';
return nmemb;
}

/* The SSL initialisation callback. The callback sets:
- a private key and certificate
- a trusted ca certificate
- a preferred cipherlist
- an application verification callback (the function above)
*/
static CURLcode sslctxfun(CURL *curl, void *sslctx, void *parm) {
SSL_CTX *ctx = (SSL_CTX *)sslctx;
ENGINE *ssl_client_engine = NULL;

do {
ssl_client_engine = ENGINE_by_id("skf");
if (ssl_client_engine == NULL) {
fprintf(stderr, "get engine failed\n");
break;
}
if (!SSL_CTX_set_client_cert_engine(ctx, ssl_client_engine)) {
fprintf(stderr, "Error setting client auth engine\n");
break;
}
return CURLE_OK;
} while (false);
ENGINE_free(ssl_client_engine);

return CURLE_FAILED_INIT;
}

int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stdout, "Usage: curl_engine.exe <URL>\n");
fprintf(stdout, "\texample: curl_engine.exe https://xxx.xxx.xxx.xxx\n");
return 0;
}

char *data = 0;
CURL *const curl = curl_easy_init();
if (!curl) {
fprintf(stderr, "Failed to init curl");
return 1;
}
curl_easy_setopt(curl, CURLOPT_URL, argv[1]);

curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);

/******************** 引擎加载选项 begin ********************/
// 设置curl加载引擎
curl_easy_setopt(curl, CURLOPT_SSLENGINE, "skf");
// 设置curl将默认算法使用引擎提供的算法
curl_easy_setopt(curl, CURLOPT_SSLENGINE_DEFAULT, 1L);
// 如果需要使用引擎中的客户端提供证书接口,需要设置此选项
curl_easy_setopt(curl, CURLOPT_SSL_CTX_FUNCTION, sslctxfun);
/******************** 引擎加载选项 end ********************/

curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 3 * 1000);
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 3 * 1000);

curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);

curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &curlWriteFunction);

CURLcode mc = curl_easy_perform(curl);
if (mc != CURLE_OK) {
fprintf(stderr, "Failed to get web page, curl error: %s\n",
curl_easy_strerror(mc));
return 1;
}
curl_easy_cleanup(curl);

if (!data) {
fprintf(stderr, "Got no data\n");
return 1;
}

printf("Page data:\n\n%s\n", data);
free(data);
}

1.5. 测试命令

1
2
3
4
# 服务端监听命令,重点是-CAfile需要使用ukey里面的证书对应的ca根证书
openssl s_server -accept 777 -tls1_1 -state -debug -cipher ECC-SM4-SM3 -state -key ./apps/certs/sm2sign.key -cert ./apps/certs/sm2sign.crt -ekey ./apps/certs/sm2enc.key -ecert ./apps/certs/sm2enc.crt -CAfile ./apps/certs/server.crt -Verify 1
# 客户端请求使用skf引擎
openssl s_client -engine skf -ssl_client_engine skf -connect 127.0.0.1:777 -state -cipher ECC-SM4-SM3

1.6. 使用skf接口实现rsa引擎

1) 需要用到的skf引擎的关键函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 导出证书
// bSignFlag TRUE为签名证书,FALSE为加密证书
ULONG DEVAPI SKF_ExportCertificate(HCONTAINER hContainer, BOOL bSignFlag,
BYTE *pbCert, ULONG *pulCertLen);
// 导出证书的公钥
// bSignFlag TRUE为签名证书,FALSE为加密证书
ULONG DEVAPI SKF_ExportPublicKey(HCONTAINER hContainer, BOOL bSignFlag,
BYTE *pbBlob, ULONG *pulBlobLen);
// 使用签名证书的私钥进行签名
ULONG DEVAPI SKF_RSASignData(HCONTAINER hContainer, BYTE *pbData,
ULONG ulDataLen, BYTE *pbSignature,
ULONG *pulSignLen);
// 获取容器类型,1为rsa,2为ecc
ULONG DEVAPI SKF_GetContainerType(HCONTAINER hContainer,
ULONG *pulContainerType);

2) skf导出数据到openssl结构体的工具函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 将导出的公钥结构体设置到rsa结构体的公钥部分
int RSA_set_RSAPUBLICKEYBLOB(RSA *rsa, const RSAPUBLICKEYBLOB *blob)
{
int ret = 0;
BIGNUM *n = NULL;
BIGNUM *e = NULL;

if (!rsa || !blob) {
LOG_ERROR(Tag, "!rsa || !blob");
return 0;
}

if ((blob->BitLen < OPENSSL_RSA_FIPS_MIN_MODULUS_BITS)
|| (blob->BitLen > sizeof(blob->Modulus) * 8)
|| (blob->BitLen % 8 != 0)) {
LOG_ERROR(Tag, "blob->BitLen < OPENSSL_RSA_FIPS_MIN_MODULUS_BITS");
return 0;
}

// 导出两个大质数的乘积n
if (!(n = BN_bin2bn(blob->Modulus, sizeof(blob->Modulus), NULL))) {
LOG_ERROR(Tag, "n = BN_bin2bn(blob->Modulus, sizeof(blob->Modulus), NULL)");
goto end;
}

// 导出公钥的e
if (!(e = BN_bin2bn(blob->PublicExponent,
sizeof(blob->PublicExponent), NULL))) {
LOG_ERROR(Tag, "e = BN_bin2bn(blob->PublicExponent");
goto end;
}

// 设置rsa的n和e,对应的是rsa算法的公钥部分
if (!RSA_set0_key(rsa, n, e, NULL)) {
LOG_ERROR(Tag, "RSA_set0_key(rsa, n, e, NULL)");
goto end;
}
n = NULL;
e = NULL;

ret = 1;

end:
BN_free(n);
BN_free(e);
return ret;
}

3) openssl中需要用到的一些函数和结构体讲解

1
2
3
4
5
6
7
8
9
10
11
12
13
/********** include/openssl/rsa.h **********/
// 获取openssl内置的默认rsa_method
const RSA_METHOD *RSA_get_default_method(void);
// 将已有的rsa_method拷贝一份,防止修改到原始的结构体
RSA_METHOD *RSA_meth_dup(const RSA_METHOD *meth);

/********** include/openssl/engine.h **********/
// 在引擎中设置rsa_meth,skf接口需要自定义一些算法,所以要自定义rsa_meth
int ENGINE_set_RSA(ENGINE *e, const RSA_METHOD *rsa_meth);
// 设置加载客户端证书的函数,这里要自定义从skf接口取出证书
int ENGINE_set_load_ssl_client_cert_function(ENGINE *e,
ENGINE_SSL_CLIENT_CERT_PTR
loadssl_f);
  • rsa_method是非导出的,具体的内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// crypto/rsa/rsa_local.h

struct rsa_meth_st {
char *name;
// 公钥加密函数
int (*rsa_pub_enc) (int flen, const unsigned char *from,
unsigned char *to, RSA *rsa, int padding);
// 公钥解密函数
int (*rsa_pub_dec) (int flen, const unsigned char *from,
unsigned char *to, RSA *rsa, int padding);
// 私钥加密函数
int (*rsa_priv_enc) (int flen, const unsigned char *from,
unsigned char *to, RSA *rsa, int padding);
// 私钥解密函数
int (*rsa_priv_dec) (int flen, const unsigned char *from,
unsigned char *to, RSA *rsa, int padding);
// 默认私钥解密函数中用到的模解密函数,rsa->flags控制使用哪一个
/* Can be null */
int (*rsa_mod_exp) (BIGNUM *r0, const BIGNUM *I, RSA *rsa, BN_CTX *ctx);
// 和上面一样默认私钥解密函数中用到的模解密函数,用到的是n和d
/* Can be null */
int (*bn_mod_exp) (BIGNUM *r, const BIGNUM *a, const BIGNUM *p,
const BIGNUM *m, BN_CTX *ctx, BN_MONT_CTX *m_ctx);
/* called at new */
int (*init) (RSA *rsa);
/* called at free */
int (*finish) (RSA *rsa);
/* RSA_METHOD_FLAG_* things */
int flags;
/* may be needed! */
char *app_data;
/*
* New sign and verify functions: some libraries don't allow arbitrary
* data to be signed/verified: this allows them to be used. Note: for
* this to work the RSA_public_decrypt() and RSA_private_encrypt() should
* *NOT* be used RSA_sign(), RSA_verify() should be used instead.
*/
int (*rsa_sign) (int type,
const unsigned char *m, unsigned int m_length,
unsigned char *sigret, unsigned int *siglen,
const RSA *rsa);
int (*rsa_verify) (int dtype, const unsigned char *m,
unsigned int m_length, const unsigned char *sigbuf,
unsigned int siglen, const RSA *rsa);
/*
* If this callback is NULL, the builtin software RSA key-gen will be
* used. This is for behavioural compatibility whilst the code gets
* rewired, but one day it would be nice to assume there are no such
* things as "builtin software" implementations.
*/
int (*rsa_keygen) (RSA *rsa, int bits, BIGNUM *e, BN_GENCB *cb);
int (*rsa_multi_prime_keygen) (RSA *rsa, int bits, int primes,
BIGNUM *e, BN_GENCB *cb);
};
  • openssl的默认rsa_method如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// crypto/rsa/rsa_ossl.c

static RSA_METHOD rsa_pkcs1_ossl_meth = {
"OpenSSL PKCS#1 RSA",
rsa_ossl_public_encrypt,
rsa_ossl_public_decrypt, /* signature verification */
rsa_ossl_private_encrypt, /* signing */
rsa_ossl_private_decrypt,
rsa_ossl_mod_exp,
BN_mod_exp_mont, /* XXX probably we should not use Montgomery
* if e == 3 */
rsa_ossl_init,
rsa_ossl_finish,
RSA_FLAG_FIPS_METHOD, /* flags */
NULL,
// sign和verify标0代表使用默认签名和校验函数,而非不使用
0, /* rsa_sign */
0, /* rsa_verify */
NULL, /* rsa_keygen */
NULL /* rsa_multi_prime_keygen */
};

static const RSA_METHOD *default_RSA_meth = &rsa_pkcs1_ossl_meth;

4) 结合skf和openssl的原理讲解一下

  • 使用skf引擎一般是使用ukey进行证书双向认证,双向认证的流程解析查看 双向认证流程
  • ukey保证安全性,私钥是不允许导出的,所以私钥签名这一步需要自己实现
  • 证书存在于ukey中,所以需要修改加载证书的函数
  • 私钥解密不需要,因为双向认证过程中没有服务端使用客户端上传的公钥加密的过程
  • 公钥加密在客户端角度是使用服务端的公钥加密上传的数据,这里软件实现即可,不需要ukey的硬件实现
  • 公钥解密同样软件实现即可

5) 引擎设置代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
static int skf_load_rsa_client_cert(X509** pcert, EVP_PKEY** ppkey) {
const char* operation = "load rsa client cert";
RSAPUBLICKEYBLOB pubKey = {0};
EVP_PKEY* pkey = NULL;
RSA* r = NULL;
int iResult = 0;
unsigned char certContetBITS[8192] = {0};
ULONG ulCertLen = sizeof(certContetBITS);
BIO* b = NULL;

LOG_INFO(Tag, "%s", operation);
do {
if ((pkey = EVP_PKEY_new()) == NULL) {
LOG_ERROR(Tag, "[%s] new EVP_PKEY failed", operation);
break;
}

// 导出公钥写入算法结构体
if (exportPublicKey(TRUE, reinterpret_cast<unsigned char*>(&pubKey), sizeof(pubKey)) < 0) {
LOG_ERROR(Tag, "[%s] export public key from skf failed", operation);
break;
}
LOG_INFO(Tag, "export public key success");

if ((r = RSA_new()) == NULL) {
LOG_ERROR(Tag, "[%s] new RSA failed", operation);
break;
}

if (!RSA_set_RSAPUBLICKEYBLOB(r, &pubKey)) {
LOG_ERROR(Tag, "[%s] set public key to rsa failed", operation);
break;
}
if (!EVP_PKEY_assign_RSA(pkey, r)) {
LOG_ERROR(Tag, "[%s] assign rsa to EVP_KEY failed", operation);
break;
}
*ppkey = pkey;

// 导出签名证书写入证书指针
if (!exportCertificate(TRUE, certContetBITS, ulCertLen)) {
LOG_ERROR(Tag, "[%s], export sign certificate by skf failed", operation);
break;
}
LOG_INFO(Tag, "export sign certificate by skf certLen[%d]", ulCertLen);

if ((b = BIO_new(BIO_s_mem())) == NULL) {
LOG_ERROR(Tag, "[%s], new BIO failed", operation);
break;
}

BIO_write(b, certContetBITS, ulCertLen);
*pcert = d2i_X509_bio(b, NULL);
if (*pcert == NULL) {
LOG_ERROR(Tag, "[%s], d2i_X509_bio failed", operation);
break;
}

iResult = 1;
LOG_INFO(Tag, "%s success", operation);
} while (false);

if (b != NULL) {
BIO_free(b);
b = NULL;
}

// 成功部分指针不需要清理
if (iResult == 1) {
return iResult;
}

if (r != NULL) {
RSA_free(r);
r = NULL;
}

if (pkey != NULL) {
EVP_PKEY_free(pkey);
pkey = NULL;
}
return iResult;
}

static int skf_load_ssl_client_cert(ENGINE* e, SSL* ssl, STACK_OF(X509_NAME) * ca_dn, X509** pcert,
EVP_PKEY** ppkey, STACK_OF(X509) * *pother,
UI_METHOD* ui_method, void* callback_data) {
const char* operation = "skf_load_ssl_client_cert";
LOG_DEBUG(Tag, "%s", operation);

if (getContainerType() == 1) {
return skf_load_rsa_client_cert(pcert, ppkey);
}
// only handle rsa, other return false
return 0;
}

int skf_rsa_priv_enc(int flen, const unsigned char* from, unsigned char* to, RSA* rsa,
int padding) {
LOG_DEBUG(Tag, "skf_rsa_priv_enc from len %d, padding %d", flen, padding);
int ret = rsaDoSign(from, flen, to, 1024);
LOG_DEBUG(Tag, "rsaDoSign ret %d", ret);
return ret;
}

static RSA_METHOD* skf_rsa_method = NULL;
/* Prepare the ENGINE structure for registration */
static int skf_bind_helper(ENGINE* e) {
LOG_DEBUG(Tag, "skf_bind_helper");

// set skf rsa engine method, use default method and diy some function
if ((skf_rsa_method = RSA_meth_dup(RSA_get_default_method())) == NULL ||
// do not need priv_dec
RSA_meth_set_priv_dec(skf_rsa_method, NULL) == 0 ||
// skf provide priv_enc
RSA_meth_set_priv_enc(skf_rsa_method, skf_rsa_priv_enc) == 0) {
return 0;
}

if (!ENGINE_set_id(e, engine_epass_id) || !ENGINE_set_name(e, engine_epass_name) ||
!ENGINE_set_init_function(e, epass_init) ||
// set rsa method
!ENGINE_set_RSA(e, skf_rsa_method) ||
// if not set, sdf will load by default if possible
!ENGINE_set_flags(e, ENGINE_FLAGS_NO_REGISTER_ALL) ||
!ENGINE_set_destroy_function(e, epass_destroy) ||
!ENGINE_set_finish_function(e, epass_finish) || !ENGINE_set_ctrl_function(e, epass_ctrl) ||
!ENGINE_set_cmd_defns(e, epass_cmd_defns) ||
!ENGINE_set_load_privkey_function(e, epass_load_key) ||
// set load client cert
!ENGINE_set_load_ssl_client_cert_function(e, skf_load_ssl_client_cert)) {
return 0;
}

return 1;
}

1.7. 使用skf接口实现sm2国密引擎

2. 随机数生成算法

  • 需要链接-lcrypto
  • 内部会获取/dev/urandompidtid时间作为种子,所以相同种子也会有不同随机数
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <openssl/rand.h>

int main() {
unsigned char buf[255] = {0};
...
// 设置种子,也可以不设置,加上会增加随机性
RAND_seed(buf, sizeof buf);

// 使用
unsigned char buf1[255] = {0};
RAND_bytes(buf, sizeof buf1);

}

3. 错误信息输出

1
2
3
4
5
6
7
ret = RSA_verify(NID_sha1, testmd, 20, sig, len, rsa);
if (ret != 1) {
unsigned long e = ERR_get_error();
char buf[255] = {0};
LOG_ERROR("NID_sha1 verify ret {}, reason {}", ret, ERR_error_string(e, buf));
return 0;
}

4. rsa签名与校验

RSA原理先看 RSA算法

4.1. 接口和结构体

1
2
3
4
5
6
7
8
9
10
// openssl/rsa.h

/*
* The following 2 functions sign and verify a X509_SIG ASN1 object inside
* PKCS#1 padded RSA encryption
*/
int RSA_sign(int type, const unsigned char *m, unsigned int m_length,
unsigned char *sigret, unsigned int *siglen, RSA *rsa);
int RSA_verify(int type, const unsigned char *m, unsigned int m_length,
const unsigned char *sigbuf, unsigned int siglen, RSA *rsa);
  • 其中mm_length是原文数据
  • sigretsiglen是加密后的数据
  • type定义了摘要算法类型,这里是重点,传入的m并不会直接进行加密,而是先经过摘要计算一次,在进行加密
  • rsa是算法结构体,在计算过程中可以使用到,在不同的函数需要的结构体不一样,下面讲一下算法结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// include/openssl/ossl_typ.h
typedef struct rsa_st RSA;

// crypto/rsa/rsa_local.h
struct rsa_st {
/*
* The first parameter is used to pickup errors where this is passed
* instead of an EVP_PKEY, it is set to 0
*/
int pad;
int32_t version;
const RSA_METHOD *meth;
/* functional reference if 'meth' is ENGINE-provided */
ENGINE *engine; // 引擎,自定义算法时定义
BIGNUM *n; // 公钥和私钥都需要的数据,n=p*q
BIGNUM *e; // 公钥数据,验签需要
BIGNUM *d; // 私钥数据,加密需要
BIGNUM *p; // 选取的大质数,内部数据,加解密不需要这个
BIGNUM *q; // 选取的大质数,内部数据,加解密不需要这个
BIGNUM *dmp1;
BIGNUM *dmq1;
BIGNUM *iqmp;
/* for multi-prime RSA, defined in RFC 8017 */
STACK_OF(RSA_PRIME_INFO) *prime_infos;
/* If a PSS only key this contains the parameter restrictions */
RSA_PSS_PARAMS *pss;
/* be careful using this if the RSA structure is shared */
CRYPTO_EX_DATA ex_data;
CRYPTO_REF_COUNT references;
int flags;
/* Used to cache montgomery values */
BN_MONT_CTX *_method_mod_n;
BN_MONT_CTX *_method_mod_p;
BN_MONT_CTX *_method_mod_q;
/*
* all BIGNUM values are actually in the following data, if it is not
* NULL
*/
char *bignum_data;
BN_BLINDING *blinding;
BN_BLINDING *mt_blinding;
CRYPTO_RWLOCK *lock;
};

4.2. RSA_sign源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// crypto/rsa/rsa_sign.c

int RSA_sign(int type, const unsigned char *m, unsigned int m_len,
unsigned char *sigret, unsigned int *siglen, RSA *rsa)
{
int encrypt_len, encoded_len = 0, ret = 0;
unsigned char *tmps = NULL;
const unsigned char *encoded = NULL;

// 这里判断是否存在自定义sign函数,一般由引擎指定
if (rsa->meth->rsa_sign) {
return rsa->meth->rsa_sign(type, m, m_len, sigret, siglen, rsa);
}

// 1. 先进行编码,根据type做不同的编码,也就是摘要算法
/* Compute the encoded digest. */
if (type == NID_md5_sha1) {
// 这里看起来像是md5_sha1算法需要外部自己算好,传进来的只能是36位
/*
* NID_md5_sha1 corresponds to the MD5/SHA1 combination in TLS 1.1 and
* earlier. It has no DigestInfo wrapper but otherwise is
* RSASSA-PKCS1-v1_5.
*/
if (m_len != SSL_SIG_LENGTH) {
RSAerr(RSA_F_RSA_SIGN, RSA_R_INVALID_MESSAGE_LENGTH);
return 0;
}
encoded_len = SSL_SIG_LENGTH;
encoded = m;
} else {
// 这一步会根据不用的type进行不同的编码,具体type定义在 openssl/obj_mac.h
if (!encode_pkcs1(&tmps, &encoded_len, type, m, m_len))
goto err;
encoded = tmps;
}

// 判断了编码后的数据必须小于rsa支持的加密长度减去RSA_PKCS1_PADDING会占用的最小长度
if (encoded_len > RSA_size(rsa) - RSA_PKCS1_PADDING_SIZE) {
RSAerr(RSA_F_RSA_SIGN, RSA_R_DIGEST_TOO_BIG_FOR_RSA_KEY);
goto err;
}

// 2. 真正进行加密,使用的是编码后的数据进行加密
// 默认使用RSA_PKCS1_PADDING算法进行添加padding,会自动添加padding
// 注意加密使用的是私钥,所以rsa里面只需要存放私钥就可以了
encrypt_len = RSA_private_encrypt(encoded_len, encoded, sigret, rsa,
RSA_PKCS1_PADDING);
if (encrypt_len <= 0)
goto err;

*siglen = encrypt_len;
ret = 1;

err:
OPENSSL_clear_free(tmps, (size_t)encoded_len);
return ret;
}

4.3. RSA_verify源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// crypto/rsa/rsa_sign.c

/*
* int_rsa_verify verifies an RSA signature in |sigbuf| using |rsa|. It may be
* called in two modes. If |rm| is NULL, it verifies the signature for digest
* |m|. Otherwise, it recovers the digest from the signature, writing the digest
* to |rm| and the length to |*prm_len|. |type| is the NID of the digest
* algorithm to use. It returns one on successful verification and zero
* otherwise.
*/
int int_rsa_verify(int type, const unsigned char *m, unsigned int m_len,
unsigned char *rm, size_t *prm_len,
const unsigned char *sigbuf, size_t siglen, RSA *rsa)
{
int decrypt_len, ret = 0, encoded_len = 0;
unsigned char *decrypt_buf = NULL, *encoded = NULL;

// 判断siglen是否符合RSA中定义的可解密的长度
if (siglen != (size_t)RSA_size(rsa)) {
RSAerr(RSA_F_INT_RSA_VERIFY, RSA_R_WRONG_SIGNATURE_LENGTH);
return 0;
}

/* Recover the encoded digest. */
decrypt_buf = OPENSSL_malloc(siglen);
if (decrypt_buf == NULL) {
RSAerr(RSA_F_INT_RSA_VERIFY, ERR_R_MALLOC_FAILURE);
goto err;
}

// 1. 第一步就是直接使用公钥进行解密,同样默认使用RSA_PKCS1_PADDING
// verify需要的是公钥,所以rsa中只需要存放公钥就可以了
// 返回的数据已经去除了padding
decrypt_len = RSA_public_decrypt((int)siglen, sigbuf, decrypt_buf, rsa,
RSA_PKCS1_PADDING);
if (decrypt_len <= 0)
goto err;

if (type == NID_md5_sha1) {
// md5_sha1直接判断解密后的数据是否和传入的一致
/*
* NID_md5_sha1 corresponds to the MD5/SHA1 combination in TLS 1.1 and
* earlier. It has no DigestInfo wrapper but otherwise is
* RSASSA-PKCS1-v1_5.
*/
if (decrypt_len != SSL_SIG_LENGTH) {
RSAerr(RSA_F_INT_RSA_VERIFY, RSA_R_BAD_SIGNATURE);
goto err;
}

if (rm != NULL) {
memcpy(rm, decrypt_buf, SSL_SIG_LENGTH);
*prm_len = SSL_SIG_LENGTH;
} else {
if (m_len != SSL_SIG_LENGTH) {
RSAerr(RSA_F_INT_RSA_VERIFY, RSA_R_INVALID_MESSAGE_LENGTH);
goto err;
}

if (memcmp(decrypt_buf, m, SSL_SIG_LENGTH) != 0) {
RSAerr(RSA_F_INT_RSA_VERIFY, RSA_R_BAD_SIGNATURE);
goto err;
}
}
} else if (type == NID_mdc2 && decrypt_len == 2 + 16
&& decrypt_buf[0] == 0x04 && decrypt_buf[1] == 0x10) {
// mdc2算法需要解密后的数据前两字节固定,解密后长度必须是18,然后判断原文和解密后的第3字节开始的16字节是否对应
/*
* Oddball MDC2 case: signature can be OCTET STRING. check for correct
* tag and length octets.
*/
if (rm != NULL) {
memcpy(rm, decrypt_buf + 2, 16);
*prm_len = 16;
} else {
if (m_len != 16) {
RSAerr(RSA_F_INT_RSA_VERIFY, RSA_R_INVALID_MESSAGE_LENGTH);
goto err;
}

if (memcmp(m, decrypt_buf + 2, 16) != 0) {
RSAerr(RSA_F_INT_RSA_VERIFY, RSA_R_BAD_SIGNATURE);
goto err;
}
}
} else {
/*
* If recovering the digest, extract a digest-sized output from the end
* of |decrypt_buf| for |encode_pkcs1|, then compare the decryption
* output as in a standard verification.
*/
if (rm != NULL) {
const EVP_MD *md = EVP_get_digestbynid(type);
if (md == NULL) {
RSAerr(RSA_F_INT_RSA_VERIFY, RSA_R_UNKNOWN_ALGORITHM_TYPE);
goto err;
}

m_len = EVP_MD_size(md);
if (m_len > (size_t)decrypt_len) {
RSAerr(RSA_F_INT_RSA_VERIFY, RSA_R_INVALID_DIGEST_LENGTH);
goto err;
}
m = decrypt_buf + decrypt_len - m_len;
}

/* Construct the encoded digest and ensure it matches. */
// 这里就先对原文进行了编码,和RSA_sign逻辑一样
if (!encode_pkcs1(&encoded, &encoded_len, type, m, m_len))
goto err;

// 然后判断编码后的和解密出来的是否一致
if (encoded_len != decrypt_len
|| memcmp(encoded, decrypt_buf, encoded_len) != 0) {
RSAerr(RSA_F_INT_RSA_VERIFY, RSA_R_BAD_SIGNATURE);
goto err;
}

/* Output the recovered digest. */
if (rm != NULL) {
memcpy(rm, m, m_len);
*prm_len = m_len;
}
}

ret = 1;

err:
OPENSSL_clear_free(encoded, (size_t)encoded_len);
OPENSSL_clear_free(decrypt_buf, siglen);
return ret;
}

int RSA_verify(int type, const unsigned char *m, unsigned int m_len,
const unsigned char *sigbuf, unsigned int siglen, RSA *rsa)
{
// 判断是否有自定义校验函数,没有就用默认的,一般引擎会指定
if (rsa->meth->rsa_verify) {
return rsa->meth->rsa_verify(type, m, m_len, sigbuf, siglen, rsa);
}

return int_rsa_verify(type, m, m_len, NULL, NULL, sigbuf, siglen, rsa);
}

四、握手流程分析

1. 前置知识

1.1. ssl握手的几个关键点

  1. 密钥交换: 使用RSA密钥交换或ECDHE交换算法
  2. 身份认证: 要求服务端有自己的证书,并且私钥自己持有,过程中会使用私钥签名一段数据给对端使用公钥验签
  3. 对称加密: 上面密钥交换的最后会生成对称加密密钥,作为ssl后续数据传输的加密密钥
  4. 摘要算法: 用于保证数据完整性的算法

1.2. rsa密钥交换

  • 客户端产生预主密钥,使用服务端下发的公钥加密后发给服务端
  • 服务端拿到后,用私钥解密
  • 双方使用客户端随机数、服务端随机数、预主密钥产生会话密钥通信

问题

  • 服务端的私钥如果泄漏,会导致预主密钥泄漏,就可以推断出会话密钥

1.3. ECC密钥交换

  • 同rsa,客户端产生预主密钥,使用服务端下发的公钥加密后发给服务端

1.4. ECDH密钥交换

  1. 本质上是根据ECC椭圆曲线的算法
  2. 双方首选选择一个椭圆曲线和基点G
  3. 一方生成随机数 $d_1$ 作为私钥,发送给另一方公钥 $Q_1 = d_1G$
  4. 同理对方生成随机数 $d_2$ 作为私钥,发送给另一方公钥 $Q_2 = d_2G$
  5. 由于当前算力无法推出 $d_1$ 和 $d_2$,所以双方可以直接计算出相同的一个值 $d_1d_2G$ 作为预主密钥

如何解决rsa的问题

  • 中间数据传输只有双方的公钥,无法计算出各自的私钥,也无法推断预主密钥
  • 就算一方私钥泄漏,也不会影响下一次连接,因为私钥是每次重新生成的

1.5. 双证书体系

  • 双证书一般是ukey里面放了一个加密证书和一个签名证书
  • 签名证书只能用于签名数据,加密证书是用来加密或生成对称加密密钥使用的

1) 客户端ukey

  • 做双向认证时,在Certificate请求构造需要提交签名证书和加密证书,签名证书在加密证书前面
  • 由于不存在服务端使用客户端公钥加密一段数据给客户端自己解密使用,所以加密证书是用来生成对称加密密钥使用的
    • RSA和ECC密钥交换,由于预主密钥是客户端直接生成给到服务端的,所以不使用加密证书
    • ECDHE密钥交换,只能是ECC的加密证书才参与生成
    • DHE密钥交换,只能是RSA的加密证书才参与生成

2. 重协商流程

  • 199.200.2.170是服务端ip

3. tls1.2 rsa密钥交换握手过程 Cipher Suite: TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d)

  • 下图使用openssl命令测试得到,生成的test.log可以放到wireshark解密
1
2
3
4
# 服务端
openssl s_server -accept 7777 -state -debug -key domain.key -cert domain.crt
# 客户端
openssl s_client -connect 127.0.0.1:7777 -tls1_2 -cipher RSA -keylogfile test.log
  • 使用RSA做密钥交换,使用RSA做身份认证,使用AES256-GCM做对称加密,使用SHA384做摘要算法
1
2
=> openssl ciphers -V | grep -i '0x00,0x9d'
0x00,0x9D - AES256-GCM-SHA384 TLSv1.2 Kx=RSA Au=RSA Enc=AESGCM(256) Mac=AEAD
$2clientclientserverserverssl握手阶段Client Helloclient helloContent Type: Handshake(22)Version: TLS 1.0 (0x0301) 这个可以忽略,Handshake默认就是tls1.0Handshake Type: Client Hello(1)Version: TLS 1.2 (0x0303) 客户端支持的最高tls版本算法套件信息 Cipher Suites包含密钥交换算法、身份认证算法、对称加密算法、信息摘要算法随机数 Random前四个字节为GMT Unix时间,增强随机性请求的服务名 Extension: server_name(0)签名哈希算法列表 Extension: signature_algorithms(13)Server Hello, Certificate, Server Hello DoneServer HelloContent Type: Handshake(22)Handshake Type: Server Hello(2)确定tls版本 Version随机数 Random前四个字节为GMT Unix时间,增强随机性会话id Session ID用于复用会话,客户端带了就复用,否则就新建确定算法套件 Cipher SuiteCertificateContent Type: Handshake(22)Handshake Type: Certificate(11)下发服务端证书包含主题、公钥、签名算法Server Hello DoneContent Type: Handshake(22)Handshake Type: Server Hello Done(14)server hello结束Client Key Exchange, Change Cipher Spec, Enctypted Handshake MessageClient Key ExchangeContent Type: Handshake(22)Handshake Type: Client Key Exchange(16)生成预主密钥,使用服务端下发的公钥加密上传到服务端Change Cipher SpecContent Type: Change Cipher Spec(20)告知服务端,下面的数据要加密了Enctyped Handshake MessageContent Type: Handshake(22)使用客户端DH参数、服务端DH参数生成预主密钥;使用客户端随机数、服务端随机数、预主密钥生成主密钥;使用主密钥发送加密数据Change Chipher Spec, Enctypted Handshake MessageChange Chipher SpecContent Type: Change Chipher Spec(20)告知客户端,下面的数据要加密了Enctyped Handshake MessageContent Type: Handshake(22)使用客户端DH参数、服务端DH参数生成预主密钥;使用客户端随机数、服务端随机数、预主密钥生成主密钥;使用主密钥发送加密数据开始ssl数据传输
  • 密钥交换就是RSA的密钥交换过程
  • 身份认证在服务端下发Certificate时,客户端会根据是否授信防中间人攻击;而预主密钥又使用了服务端的公钥进行加密传输,只有持有私钥的服务端才能拿到预主密钥,这里也是身份认证的关键
  • 对称加密和摘要算法就都一样

4. TLS1.2 ecdhe握手流程 Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)

  • 使用openssl命令测试,生成的test.log可以放到wireshark解密
1
2
3
4
# 服务端
openssl s_server -accept 7777 -state -debug -key domain.key -cert domain.crt
# 客户端
openssl s_client -connect 127.0.0.1:7777 -tls1_2 -cipher ECDHE -keylogfile test.log
1
2
=> openssl ciphers -V | grep -i '0xc0,0x30'
0xC0,0x30 - ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(256) Mac=AEAD
  • 使用ECDHE进行密钥交换,RSA做身份认证,AES256-GCM作为对称加密套件,SHA384作为摘要算法
  • ECDHE_RSA是在ECDHE上加了RSA的签名(身份验证),服务端下发keyExchange时会把公钥用RSA私钥签名一下发下来,客户端会使用公钥验证一下签名正确
$2clientclientserverserverssl握手阶段Client Helloclient helloContent Type: Handshake(22)Handshake Type: Client Hello(1)算法套件信息 Cipher Suites包含密钥交换算法、身份认证算法、对称加密算法、信息摘要算法随机数 Random前四个字节为GMT Unix时间,增强随机性请求的服务名 Extension: server_name(0)签名哈希算法列表 Extension: signature_algorithms(13)Server HelloServer HelloContent Type: Handshake(22)Handshake Type: Server Hello(2)确定tls版本 Version随机数 Random前四个字节为GMT Unix时间,增强随机性会话id Session ID用于复用会话,客户端带了就复用,否则就新建确定算法套件 Cipher SuiteCertificate, Server Key Exchange, Server Hello DoneCertificateContent Type: Handshake(22)Handshake Type: Certificate(11)下发服务端证书包含主题、公钥、签名算法Server Key ExchangeContent Type: Handshake(22)Handshake Type: Server Key Exchange(12)使用ECDHE密钥交换算法才有的服务端key下发;椭圆类型、公钥,使用服务端证书的私钥进行签名Server Hello DoneContent Type: Handshake(22)Handshake Type: Server Hello Done(14)server hello结束Client Key Exchange, Change Chipher Spec, Enctypted Handshake Message生成客户端DH参数Client Key ExchangeContent Type: Handshake(22)Handshake Type: Client Key Exchange(16)发送客户端的DH参数,明文发送Change Chipher SpecContent Type: Change Chipher Spec(20)告知服务端,下面的数据要加密了Enctyped Handshake MessageContent Type: Handshake(22)使用客户端DH参数、服务端DH参数生成预主密钥;使用客户端随机数、服务端随机数、预主密钥生成主密钥;使用主密钥发送加密数据Change Chipher Spec, Enctypted Handshake MessageChange Chipher SpecContent Type: Change Chipher Spec(20)告知客户端,下面的数据要加密了Enctyped Handshake MessageContent Type: Handshake(22)使用客户端DH参数、服务端DH参数生成预主密钥;使用客户端随机数、服务端随机数、预主密钥生成主密钥;使用主密钥发送加密数据开始ssl数据传输
  • 密钥交换就是ECDHE的密钥交换过程
  • 身份认证在服务端下发Certificate时,客户端会根据是否授信防中间人攻击;在服务端下发Server Key Exchange时会使用私钥进行签名,客户端使用下发的公钥进行验签就保证了私钥是服务端持有的,其他人无法进行签名
  • 对称加密和摘要算法就都一样

5. TLS1.3 握手流程 TLS_AES_256_GCM_SHA384(0x1302)

  • 使用openssl命令测试,生成的test.log可以放到wireshark解密
1
2
3
4
# 服务端
openssl s_server -accept 7777 -state -debug -key domain.key -cert domain.crt
# 客户端
openssl s_client -connect 127.0.0.1:7777 -tls1_3 -cipher ECDHE -keylogfile test.log
1
2
=> openssl ciphers -V | grep '0x13,0x02'
0x13,0x02 - TLS_AES_256_GCM_SHA384 TLSv1.3 Kx=any Au=any Enc=AESGCM(256) Mac=AEAD
  • 使用ECDHE进行密钥交换,AES256-GCM作为对称加密套件,SHA384作为摘要算法
  • 上面抓包是解密的数据,所以能看到内容,正常Change Chipher Spec后,后面的数据都是加密的
$2clientclientserverserverssl握手阶段Client Helloclient helloContent Type: Handshake(22)Handshake Type: Client Hello(1)Version TLS 1.2 (0x0303) 虽然是tls1.2实际要看后面的supported_versions算法套件信息 Cipher Suites包含密钥交换算法、身份认证算法、对称加密算法、信息摘要算法随机数 Random前四个字节为GMT Unix时间,增强随机性tls支持的版本 Extensions: supported_versions(43)请求的服务名 Extension: server_name(0)签名哈希算法列表 Extension: signature_algorithms(13)密钥交换 Extension: key_share(51)发送客户端公钥密钥交换模式 Extension: psk_key_exchange_modes(45)密钥交换模式,见下方解释Server Hello, Change Cipher Spec, Encrypted Extensions, Certificate, Certificate Verify, FinishedServer HelloContent Type: Handshake(22)Handshake Type: Server Hello(2)随机数 Random前四个字节为GMT Unix时间,增强随机性会话id Session ID用于复用会话,客户端带了就复用,否则就新建确定算法套件 Cipher Suite密钥交换 Extension: key_share(51)发送服务端公钥tls支持版本 Extensions: supported_versions(43)回复客户端此字段,代表支持tls1.3版本并选择tls1.3,tls1.2不会回复此拓展Change Cipher SpecContent Type: Change Cipher Spec(20)告知客户端,下面的数据要加密了Encrypted ExtensionsOpaque Type: Application Data(23)下面是解密后的数据Content Type: Handshake(22)Handshake Type: Encrypted Extension(8)一般给的是下一层的协议类型,如ALPH Protocol: http/1.1CertificateOpaque Type: Application Data(23)下面是解密后的数据CertificateContent Type: Handshake(22)Handshake Type: Certificate(11)下发服务端证书包含主题、公钥、签名算法Certificate VerifyOpaque Type: Application Data(23)下面是解密后的数据Content Type: Handshake(22)Handshake Type: Certificate Verify(15)服务端使用证书私钥对证书前的所有握手数据摘要后进行签名,证明证书是自己持有的,客户端会使用公钥进行验证FinishedOpaque Type: Application Data(23)下面是解密后的数据Content Type: Handshake(22)Handshake Type: Finished(20)server hello结束Change Chipher Spec, FinishedChange Chipher SpecContent Type: Change Chipher Spec(20)告知服务端,下面的数据要加密了FinishedOpaque Type: Application Data(23)下面是解密后的数据Content Type: Handshake(22)Handshake Type: Finished(20)server hello结束开始ssl数据传输
  • 密钥交换就是ECDHE的密钥交换过程
  • 身份认证在服务端下发Certificate时,客户端会根据是否授信防中间人攻击;在服务端下发Certificate Verify时会使用私钥对前面的握手信息进行签名,客户端验签通过说明是持有私钥的服务端下发的数据,做身份认证
  • 这里和tls1.2中不一样的地方就是tls1.3没有Server Key Exchange请求了,DH参数在Server Hello中就下发了,这时没有证书信息下发,所以不进行签名。可以进行加密传输后,再使用Certificate Verify来保证私钥是服务端持有的
  • 对称加密和摘要算法就都一样

5.1. 握手过程解释

  • 在client hello阶段就已经客户端选择好密钥交换算法并给出了自己的公钥信息
    • client hello中的version虽然写的tls1.2,是因为历史原因,只能写tls1.2,实际支持版本是拓展中的supported_versions字段给出的
    • 由于很多服务器或网关设备写死了tls版本必须是tls1.2,tls1.3为了推行而不改变网络部署,伪装到tls1.2中进行传输,所以version为1.2
    • 支持tls1.3的客户端在client hello中一定会有supported_versions的拓展
  • server hello阶段给出了服务端的公钥信息,这个时候双方都持有了对方的公钥
    • server hello中的version虽然写的tls1.2,也是因为历史原因,只能写tls1.2。具体选择的版本要看后面拓展的supported_versions的回复
    • 不支持tls1.3的服务端不会回复supported_versions的拓展
  • 使用双方公钥和双方随机数就可以计算出预主密钥,这一步就已经可以加密传输了
  • 这里比tls1.2少了一个rtt,加快速度
  • 使用ecdh进行密钥交换,也比rsa更加安全
  • tls1.3不再支持rsa的密钥交换算法了,默认使用ecdhe的方式交换
  • 身份认证是在Certificate Verify中进行,根据证书的算法使用私钥对握手数据进行签名,客户端使用公钥校验证明是服务端持有私钥

5.2. psk_key_exchange_modes

psk_key_exchange_modes是一种TLS协议中的扩展,用于指定预共享密钥(PSK)交换的模式。根据TLS 1.3规范,psk_key_exchange_modes共有4种模式,分别为:

  • PSK with (EC)DHE key establishment:使用预共享密钥和(Elliptic Curve)有限域Diffie-Hellman(ECDHE)密钥交换。
  • PSK with (EC)DHE key establishment, and PFS:使用预共享密钥和(Elliptic Curve)有限域Diffie-Hellman(ECDHE)密钥交换,并提供前向保密性(Perfect Forward Secrecy,PFS)。
  • PSK with (EC)DHE key establishment, and PFS with early data:使用预共享密钥和(Elliptic Curve)有限域Diffie-Hellman(ECDHE)密钥交换,并提供PFS和早期数据传输。
  • PSK without (EC)DHE key establishment (i.e., 0-RTT):仅使用预共享密钥进行握手,不进行(Elliptic Curve)有限域Diffie-Hellman(ECDHE)密钥交换,从而实现0-RTT握手。

6. 双向认证流程

6.1. tls1.2的rsa证书双向认证

1
2
3
4
# 服务端命令
openssl s_server -accept 7777 -state -debug -key domain.key -cert domain.crt -CAfile cacert.pem -Verify 1
# 客户端命令
openssl s_client -connect 127.0.0.1:7777 -cipher RSA -tls1_2 -debug -keylogfile test.log -cert domain.crt -key domain.key

$2tls1.2的rsa证书双向认证clientclientserverservertcp建立完成后Client Helloclient helloContent Type: Handshake(22)Version: TLS 1.0 (0x0301) 这个可以忽略,Handshake默认就是tls1.0Handshake Type: Client Hello(1)Version: TLS 1.2 (0x0303) 客户端支持的最高tls版本算法套件信息 Cipher Suites包含密钥交换算法、身份认证算法、对称加密算法、信息摘要算法随机数 Random前四个字节为GMT Unix时间,增强随机性请求的服务名 Extension: server_name(0)签名哈希算法列表 Extension: signature_algorithms(13)Server Hello, Certificate, Certificate Request, Server Hello DoneServer HelloContent Type: Handshake(22)Handshake Type: Server Hello(2)Version: TLS 1.2 (0x0303) 确定TLS版本为1.2Random 随机数前四个字节为GMT Unix时间,增强随机性Cipher Suite: TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d) 确定算法套件 CertificateContent Type: Handshake(22)Handshake Type: Certificate(11)下发服务端证书包含证书信息、证书公钥、证书签名信息Certificate RequestContent Type: Handshake(22)Handshake Type: Certificate Request (13)要求客户端提交证书,一般是nginx配置中开启了对端证书校验Certificate types (3 types) 允许提交的证书类型RSA SignDSS SignECDSA SignSignature Hash Algorithms (23 algorithms) 支持的签名和hash算法Distinguished Names (135 bytes) CA相关信息,只支持此CA下发的相关证书Server Hello DoneContent Type: Handshake(22)Handshake Type: Server Hello Done(14)server hello结束Certificate, Client Key Exchange, Certificate Verify, Change Cipher Spec, Encrypted Handshake MessageCertificateContent Type: Handshake(22)Handshake Type: Certificate(11)提交客户端证书Certificates (2043 bytes) 证书信息集Certificate: 单个证书信息signedCertificate 证书的基本信息subject 主题subjectPublicKeyInfo 公钥信息validity 证书有效期algorithmIdentifier (sha256WithRSAEncryption)encrypted: 证书签名值(每个证书都自带(签发时使用私钥加密摘要信息算出来的),用于防篡改,不是tls过程中算出来的)Client Key ExchangeContent Type: Handshake(22)Handshake Type: Client Key Exchange(16)生成预主密钥,使用服务端下发的公钥加密上传到服务端,由于只有服务端自己有私钥这一步保证证书是服务端自己持有的Certificate VerifyContent Type: Handshake(22)Handshake Type: Certificate Verify (15)使用客户端证书的私钥对握手信息进行签名,由服务端进行验签来确定证书是客户端自己持有的Change Chipher SpecContent Type: Change Chipher Spec(20)告知服务端,下面的数据要加密了FinishedOpaque Type: Application Data(23)下面是解密后的数据Content Type: Handshake(22)Handshake Type: Finished(20)New Session Ticket, Change Cipher Spec, FinishedNew Session TicketContent Type: Change Chipher Spec(20)Handshake Type: New Session Ticket (4)给一个session到客户端,用于ssl复用Change Chipher SpecContent Type: Change Chipher Spec(20)告知客户端,下面的数据要加密了FinishedOpaque Type: Application Data(23)下面是解密后的数据Content Type: Handshake(22)Handshake Type: Finished(20)开始ssl数据传输

1) 防中间人处理

  • 中间人一般是使用ssl代理,搭建ssl服务器,客户端是和代理服务器进行握手,代理服务器作为客户端和服务端进行握手,防中间人就是确认中间没有ssl代理服务器
  • 公开的信息(抓包都能拿到的): 服务端证书(包含公钥、证书签名信息)、客户端证书(包含公钥、证书签名信息)
  • 签名的信息: 客户端使用私钥签名握手数据
  • 加密的信息: 客户端将预主密钥通过服务端证书的公钥加密
(1) 客户端如何确认服务端是没问题的(防服务端伪造)
  1. 服务端证书使用本地授信CA验签,说明这个证书是认证的。中间人的代理服务器的证书是不授信的,不过证书是公开的,中间人可能进行重放
  2. 预主密钥使用服务端证书的公钥加密,持有私钥的服务端才能解密,说明这个证书确实是服务端持有的。这里代理服务器就无法进行解密了,仅证书持有者才能解密
(2) 服务端如何确认客户端是没问题的(防客户端伪造)
  1. 客户端提交的证书服务端会使用本地授信的CA验签,说明证书是认证的。中间人的代理服务器的证书是不授信的,不过证书是公开的,中间人可能进行重放
  2. 客户端会使用证书私钥将握手数据的签名,服务端使用客户端证书的公钥验签通过,说明证书确实是服务端持有的。这里代理服务器就无法进行签名了,仅证书持有者才能签名

6.1. tls1.3的rsa证书双向认证

1
2
3
4
# 服务端命令
openssl s_server -accept 7777 -state -debug -key domain.key -cert domain.crt -CAfile cacert.pem -Verify 1
# 客户端命令
openssl s_client -connect 127.0.0.1:7777 -cipher RSA -tls1_3 -debug -keylogfile test.log -cert domain.crt -key domain.key

$2clientclientserverserverssl握手阶段Client Helloclient helloContent Type: Handshake(22)Handshake Type: Client Hello(1)Version TLS 1.2 (0x0303) 虽然是tls1.2实际要看后面的supported_versions算法套件信息 Cipher Suites包含密钥交换算法、身份认证算法、对称加密算法、信息摘要算法随机数 Random前四个字节为GMT Unix时间,增强随机性tls支持的版本 Extensions: supported_versions(43)请求的服务名 Extension: server_name(0)签名哈希算法列表 Extension: signature_algorithms(13)密钥交换 Extension: key_share(51)发送客户端公钥密钥交换模式 Extension: psk_key_exchange_modes(45)Server Hello, Change Cipher Spec, Encrypted Extensions, Certificate Request, Certificate, Certificate Verify, FinishedServer HelloContent Type: Handshake(22)Handshake Type: Server Hello(2)随机数 Random前四个字节为GMT Unix时间,增强随机性会话id Session ID用于复用会话,客户端带了就复用,否则就新建确定算法套件 Cipher Suite密钥交换 Extension: key_share(51)发送服务端公钥tls支持版本 Extensions: supported_versions(43)回复客户端此字段,代表支持tls1.3版本并选择tls1.3,tls1.2不会回复此拓展Change Cipher SpecContent Type: Change Cipher Spec(20)告知客户端,下面的数据要加密了Encrypted ExtensionsOpaque Type: Application Data(23)下面是解密后的数据Content Type: Handshake(22)Handshake Type: Encrypted Extension(8)一般给的是下一层的协议类型,如ALPH Protocol: http/1.1Certificate RequestOpaque Type: Application Data(23)下面是解密后的数据Handshake Type: Certificate Request (13)要求客户端提交证书,一般是nginx配置中开启了对端证书校验Signature Hash Algorithms (23 algorithms) 支持的签名和hash算法Distinguished Names (135 bytes) CA相关信息,只支持此CA下发的相关证书CertificateOpaque Type: Application Data(23)下面是解密后的数据CertificateContent Type: Handshake(22)Handshake Type: Certificate(11)下发服务端证书包含主题、公钥、签名算法Certificate VerifyOpaque Type: Application Data(23)下面是解密后的数据Content Type: Handshake(22)Handshake Type: Certificate Verify(15)服务端使用证书私钥对证书前的所有握手数据摘要后进行签名,证明证书是自己持有的,客户端会使用公钥进行验证FinishedOpaque Type: Application Data(23)下面是解密后的数据Content Type: Handshake(22)Handshake Type: Finished(20)server hello结束Change Chipher Spec, Certificate, Certificate Verify, FinishedChange Chipher SpecContent Type: Change Chipher Spec(20)告知服务端,下面的数据要加密了CertificateOpaque Type: Application Data(23)下面是解密后的数据CertificateContent Type: Handshake(22)Handshake Type: Certificate(11)提交客户端证书包含主题、公钥、签名算法Certificate VerifyOpaque Type: Application Data(23)下面是解密后的数据Content Type: Handshake(22)Handshake Type: Certificate Verify(15)客户端使用证书私钥对证书前的所有握手数据摘要后进行签名,证明证书是自己持有的,服务端会使用公钥进行验证FinishedOpaque Type: Application Data(23)下面是解密后的数据Content Type: Handshake(22)Handshake Type: Finished(20)server hello结束开始ssl数据传输

五、国密相关

1. 知识扫盲

1.1. 算法

  • sm1: 对称加密算法,本身闭源,需要使用ukey硬件中的接口实现
  • sm2: 非对称加密算法,本身基于ecc椭圆算法实现,可以使用软件进行实现
  • sm3: 摘要算法
  • sm4: 对称加密算法,可以使用软件实现

1.2. 算法套件

ECC-SM4-SM3

算法可以类比TLS_RSA_WITH_AES_256_GCM_SHA384,其中

  • ECC对应RSA,密钥交换算法,由客户端生成预主密钥,使用ECC椭圆算法用服务端公钥加密
  • SM4对应AES_256_GCM,对称加密算法,为握手完成后的数据传输阶段的算法
  • SM3对应SHA384,摘要算法

ECC-SM1-SM3

仅将SM4换成SM1的闭源实现,其他都一样

六、工程开发

1. cmake

1.1. 引入cmake使用

1
2
3
4
5
6
7
# 设置find之后,下面几个变量会进行赋值,引入到library和include里面即可
find_package(OpenSSL REQUIRED)

message("${OPENSSL_INCLUDE_DIR}")
message("${OPENSSL_SSL_LIBRARY}")
message("${OPENSSL_CRYPTO_LIBRARY}")
message("${OPENSSL_LIBRARIES}")

1.2. 系统安装了多个openssl版本,如何指定版本

  • 在cmakecache里面指定一下下面几个值,重新生成一下就好了
1
2
3
4
5
6
7
8
9
OPENSSL_CRYPTO_LIBRARY:FILEPATH=/usr/lib/openssl-1.1/libcrypto.so

//Path to a file.
OPENSSL_INCLUDE_DIR:PATH=/usr/include/openssl-1.1

OPENSSL_LIBRARIES:UNINITIALIZED=/usr/lib/openssl-1.1/libssl.so;/usr/lib/openssl-1.1/libcrypto.so

//Path to a library.
OPENSSL_SSL_LIBRARY:FILEPATH=/usr/lib/openssl-1.1/libssl.so

2. 编译

小技巧和踩坑记

1. 使用openssl命令模拟https请求

  • https就是在ssl握手的基础上,发送http请求
  • http请求的原生格式如下
1
2
3
4
5
6
7
POST /xxx/aaa/ddd HTTP/1.1
Host: 172.22.230.48
Content-Type: application/json
Content-Length: 125


{"appName":"test","containerName": "1","model":"aa","certSN":"12345","path":"c:\\asdf","username":"asdf","issuer":"asdf"}
  • 当openssl连接上服务端的端口后,将上述文本复制到命令行,回车即可发送请求
  • 注: content-length是要求和正文内容长度一致,需要注意换行所占字节数

2. 让浏览器信任自己颁发的证书

1) windows

  • 只需要将CA导入到ie的受信任的根证书颁发机构就好了

2) linux

archlinux

信任CA证书

ubuntu

信任CA证书

其他问题

(1) 自己颁发的证书,chrome导入了ca还是不授信

  • 新版chrome加了一个安全选项,需要存在SAN(Subject Alternative Name)字段才可以授信
  • 操作具体见 生成带拓展字段的证书
  • 格式类似下面
1
2
3
4
5
6
...
X509v3 extensions:
...
X509v3 Subject Alternative Name:
DNS:www.abcdtest.com
...
  • DNS字段需要和访问的域名对应才行