SSL/TLS 双向认证指的是相互校验,服务器需要校验每个client,client也需要校验服务器。此篇文章使用两个 ESP32 分别做 HTTPS server 和 HTTPS client 来尝试了解 ESP32 HTTPS 双向认证的实现流程。分为以下四部分:
- 客户端以及服务器端的证书生成
- 服务器端代码编写
- 客户端代码编写
- 测试验证
- 附录
1 客户端以及服务器端的证书生成
在双向认证前,需要生成客户端和服务器端的以下证书:
- server 需要 server.key 、server.crt 、ca.crt(client 端 CA 证书)
- client 需要 client.key 、client.crt 、ca.crt(server 端 CA 证书)
上述证书可以通过 openssl 指令生成,在此也可以使用总结好的脚本gen_crt.sh
,内容如下:
#!/bin/bash
#
# Generate the certificates and keys for testing.
#
SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
ROOT_SUBJECT="/C=C1/ST=JS1/L=WX1/O=ESP1/OU=ESP1/CN=Server1 CA/emailAddress=ESP1"
LEVEL2_SUBJECT="/C=C2/ST=JS22/L=WX22/O=ESP22/OU=ESP22/CN=Server22 CA/emailAddress=ESP22"
LEVEL3_SUBJECT="/C=C3/ST=JS333/L=WX333/O=ESP333/OU=ESP333/CN=Server333 CA/emailAddress=ESP333"
# private key generation
openssl genrsa -out ca.key 2048
openssl genrsa -out server.key 2048
openssl genrsa -out client.key 2048
# cert requests
openssl req -new -key ca.key -out ca.csr -text -subj $ROOT_SUBJECT
openssl req -new -key server.key -out server.csr -text -subj $LEVEL2_SUBJECT
openssl req -new -key client.key -out client.csr -text -subj $LEVEL3_SUBJECT
# generate the actual certs.
openssl x509 -req -in ca.csr -out ca.pem -sha256 -days 5000 -signkey ca.key -text -extensions v3_ca
openssl x509 -req -in server.csr -out server.pem -sha256 -CAcreateserial -days 5000 -CA ca.pem -CAkey ca.key -text -extensions v3_ca
openssl x509 -req -in client.csr -out client.pem -sha256 -CAcreateserial -days 5000 -CA ca.pem -CAkey ca.key -text -extensions v3_ca
rm *.csr
rm *.srl
mv ca.* ./main
mv server.* ./main
mv client.* ./main
具体的使用方式为:将上述脚本 gen_crt.sh
放入您需要生成 ESP HTTPS 客户端或服务器证书的 ESP-IDF 工程下,打开工程对应的终端,输入
. ./gen_crt.sh
就可以看到以下 log 信息:
[11:54:03] [~/github/esp-idf/rel4.4/esp-idf/examples/protocols/esp_http_client] git(1329b19fe4) 🔥 ❱❱❱ . ./gen_crt.sh
Generating RSA private key, 2048 bit long modulus (2 primes)
...........................................................+++++
.................+++++
e is 65537 (0x010001)
Generating RSA private key, 2048 bit long modulus (2 primes)
....+++++
...........................................................................................+++++
e is 65537 (0x010001)
Generating RSA private key, 2048 bit long modulus (2 primes)
.............................................+++++
......+++++
e is 65537 (0x010001)
Can't load /home/zhengzhong/.rnd into RNG
140139168797120:error:2406F079:random number generator:RAND_load_file:Cannot open file:../crypto/rand/randfile.c:88:Filename=/home/zhengzhong/.rnd
Can't load /home/zhengzhong/.rnd into RNG
140396157870528:error:2406F079:random number generator:RAND_load_file:Cannot open file:../crypto/rand/randfile.c:88:Filename=/home/zhengzhong/.rnd
Can't load /home/zhengzhong/.rnd into RNG
140703026512320:error:2406F079:random number generator:RAND_load_file:Cannot open file:../crypto/rand/randfile.c:88:Filename=/home/zhengzhong/.rnd
Signature ok
subject=C = C1, ST = JS1, L = WX1, O = ESP1, OU = ESP1, CN = Server1 CA, emailAddress = ESP1
Getting Private key
Signature ok
subject=C = C2, ST = JS22, L = WX22, O = ESP22, OU = ESP22, CN = Server22 CA, emailAddress = ESP22
Getting CA Private Key
Signature ok
subject=C = C3, ST = JS333, L = WX333, O = ESP333, OU = ESP333, CN = Server333 CA, emailAddress = ESP333
Getting CA Private Key
脚本运行完毕后,即可看到 ESP-IDF 对应工程下的 main 文件夹里新增了一些证书,如下:
到此,证书生成的步骤已经完成。此时:
- 客户端需要的证书为 ca.pem, client.pem, client.key
- 服务器端需要的证书为 ca.pem, server.pem, server.key
2 服务器端代码编写
这部分以 ESP-IDF v4.4.1 里的 https_server/simple example 为例,首先需要确保第一步生成的对应 ca.pem, server.pem, server.key 证书放在了 main 文件夹下:
然后需要让代码成功加载这些证书并使用,首先修改项目 main 文件夹下 CMakeLists.txt 为如下:
idf_component_register(SRCS "main.c"
INCLUDE_DIRS "."
EMBED_TXTFILES "ca.pem"
"server.pem"
"server.key")
然后替换掉 main.c 里原有的证书,修改后如下:
extern const unsigned char cacert_pem_start[] asm("_binary_server_pem_start");
extern const unsigned char cacert_pem_end[] asm("_binary_server_pem_end");
conf.cacert_pem = cacert_pem_start;
conf.cacert_len = cacert_pem_end - cacert_pem_start;
extern const unsigned char prvtkey_pem_start[] asm("_binary_server_key_start");
extern const unsigned char prvtkey_pem_end[] asm("_binary_server_key_end");
conf.prvtkey_pem = prvtkey_pem_start;
conf.prvtkey_len = prvtkey_pem_end - prvtkey_pem_start;
extern const unsigned char client_pem_start[] asm("_binary_ca_pem_start");
extern const unsigned char client_pem_end[] asm("_binary_ca_pem_end");
conf.client_verify_cert_pem = client_pem_start;
conf.client_verify_cert_len = client_pem_end - client_pem_start;
具体修改的代码地点请参考下图。
然后在 menuconfig 里配置 Wi-Fi SSID 和 Wi-Fi Password 后,烧录程序进 ESP32 即可。不过此时需要从 HTTPS server 运行的 log 里找到 server 的 IP 地址,方便 HTTPS client 后续进行连接。
在这次实践中,HTTPS server 的 IP 为 192.168.1.109。如下:
到这里,HTTPS server 端的代码编写修改工作全部结束。
3 客户端代码编写
这部分以 ESP-IDF v4.4.1 里的 esp_http_client example 为例,首先需要确保第一步生成的对应 ca.pem, client.pem, client.key 证书放在了 main 文件夹下:
然后需要让代码成功加载这些证书并使用,首先修改项目 main 文件夹下 CMakeLists.txt 为如下:
# Embed the server root certificate into the final binary
#
# (If this was a component, we would set COMPONENT_EMBED_TXTFILES here.)
idf_component_register(SRCS "esp_http_client_example.c"
INCLUDE_DIRS "."
EMBED_TXTFILES howsmyssl_com_root_cert.pem
postman_root_cert.pem
ca.pem
client.pem
client.key)
然后在 main.c 里新增对应的证书,如下:
extern const char server_root_cert_pem_start[] asm("_binary_ca_pem_start");
extern const char server_root_cert_pem_end[] asm("_binary_ca_pem_end");
extern const char client_cacert_pem_start[] asm("_binary_client_pem_start");
extern const char client_cacert_pem_pem_end[] asm("_binary_client_pem_end");
extern const char client_prvtkey_pem_start[] asm("_binary_client_key_start");
extern const char client_prvtkey_pem_end[] asm("_binary_client_key_end");
具体修改的代码地点请参考下图。
然后需要在 esp_http_client_config_t 结构体里配置使用上述证书,修改如下:
esp_http_client_config_t config = {
.url = "https://192.168.1.109:443",
.event_handler = _http_event_handler,
// .crt_bundle_attach = esp_crt_bundle_attach,
.cert_pem = server_root_cert_pem_start,
.client_cert_pem = client_cacert_pem_start,
.client_key_pem = client_prvtkey_pem_start,
.skip_cert_common_name_check = true,
};
具体修改的代码地点请参考下图。
同时也修改了上图里的 log 显示等级为 warning。方便后续观测测试结果。
注:上述 esp_http_client_config_t 结构体里的 url 成员变量里的 IP 应为实际观测到的 HTTPS server IP,在这里使用的是第二步观测到的 192.168.1.109。
最后在 menuconfig 里配置 Wi-Fi SSID 和 Wi-Fi Password 后,烧录程序进 ESP32 即可。到这里,HTTPS client 端的代码编写修改工作全部结束。
4 测试验证
同时烧录上述两个工程到两个 ESP32 并观测 log,可以看到 HTTPS 链接成功建立,如下:
5 附录
5.1 相关参考
5.2 Q & A
-
证书生成脚本里的 ROOT_SUBJECT 是什么?CN 检查 为什么要关掉
[ESP]ROOT_SUBJECT 里为用于生成 CA 证书的一些信息, CN 检查关闭的原因是客户端此时是按照 IP 访问的服务器端,如果设置 CN 检查, CN 字段为 IP 值,若服务器证书 CN 生成的时候设置成这个 IP,应该就可以访问了。总结就是 CN 字段客户端需和服务器证书的 CN 字段匹配。 -
这种情况下的 server 端的 CA 证书为啥和 client 是共用的?
[ESP]这个 CA 同时签发了客户端和服务器的证书,就可以用这个 CA 去校验服务器或者客户端证书。 -
为什么需要屏蔽掉 crt_bundle_attach = esp_crt_bundle_attach?
[ESP]crt_bundle_attach 里保存的校验服务器的 CA 证书都是受信任的 CA 证书,而自行使用 openssl 生成的 CA 证书是不受信任的,自然签发的证书也都是不受信任的,用受信任的 CA 证书链去校验不受信任的服务器证书肯定会校验失败。 -
skip_cert_common_name_check = true 是因为此时 esp32 server 没有类似于域名的 common name 吗?
[ESP]是的,对于自签证书,可以尝试将 server 端证书 CN 设置成 server 的 IP 地址,然后客户端的 URL 里面也是直接传入 IP 地址结构的 URL,此时打开 CN 检查即可校验通过。