14.4.3. 自签名证书

自签名证书是在两台由你掌控的设备之间快速跑通 TLS 的方式:两端都信任一张由你自己生成的证书。它适用于所有由你同时配置连接两端的部署场景——只有当第三方客户端必须在没有被告知去信任某张自定义证书的情况下连接进来时,公共证书颁发机构(CA)才会登场。

14.4.3.1. 创建自签名证书

在你的开发机上运行 OpenSSL。subjectAltName(SAN)是现代 TLS 客户端在主机名验证期间会检查的内容,因此请把它设置为客户端用来访问摄像头的主机名和/或 IP 地址(单独的 CN 已是遗留用法,会被许多客户端忽略)。请将 DNS:openmv / IP:192.168.1.50 替换为你的客户端实际连接的地址。

ECDSA P-256 —— 推荐:

# Generate a P-256 private key.
openssl ecparam -name prime256v1 -genkey -noout -out server.key

# Self-signed certificate valid for one year, with a SAN.
openssl req -new -x509 -key server.key -out server.crt -days 365 \
    -subj "/CN=openmv" -addext "subjectAltName=DNS:openmv,IP:192.168.1.50"

ECDSA P-384 —— 更强,但更大/更慢:

openssl ecparam -name secp384r1 -genkey -noout -out server.key

openssl req -new -x509 -key server.key -out server.crt -days 365 \
    -subj "/CN=openmv" -addext "subjectAltName=DNS:openmv,IP:192.168.1.50"

RSA-2048 —— 兼容性最佳:

openssl req -new -x509 -newkey rsa:2048 -nodes -keyout server.key \
    -out server.crt -days 365 -subj "/CN=openmv" \
    -addext "subjectAltName=DNS:openmv,IP:192.168.1.50"

备注

客户端证书(用于下文的双向身份验证)使用的正是与上面完全相同的命令来创建——证书本身并没有任何专属于客户端的东西。只需以不同的名字(例如 client.key / client.crt)再生成一对独立的密钥/证书,并按 mTLS 示例所示在客户端上使用它即可。subjectAltName 只对那一端有意义,即对端会去验证其主机名的那一端(客户端会检查服务器的名称;没有任何一方会检查客户端的名称),因此对于仅供客户端使用的证书可以省略它。同样地,-subj / CN 在客户端证书上也只是一个标签——此处服务器端只检查证书是否链接到一个受信任的 CA,从不匹配名称——所以把它设置为任何能标识该客户端的内容即可(例如 /CN=sensor-01)。无论如何都要保留一个 -subj 值,这样 OpenSSL 才能以非交互方式生成证书。

证书的有效期由 -days 设定;证书会过期,并且必须在到期之前重新生成和重新部署。

14.4.3.2. 转换为 DER

在把证书和私钥复制到摄像头之前,先将二者都转换为 DER:

openssl x509 -in server.crt -outform DER -out server.der
openssl pkey -in server.key -outform DER -out server.key.der

14.4.3.3. 将文件复制到摄像头

将 DER 文件复制到摄像头的文件系统——例如把它们拖放到 OpenMV Cam 的 USB 驱动器上,或者使用 mpremote cp server.der :mpremote cp server.key.der :。在执行验证的那一端,还要把 CA / 对端证书也以 DER 形式复制过去。

DER 文件不一定非要存放在可写文件系统上。MicroPython 也可以在 /rom 处挂载一个只读的 ROMFS 镜像,放在那里的证书会像任何其他文件一样被加载——例如 ctx.load_cert_chain("/rom/server.der", "/rom/server.key.der")。ROMFS 镜像是在你的开发机上准备好的,并且在运行时是只读的,因此证书无法在设备上被改动——这对于锁定一台生产设备很有用。请注意,存储在 ROMFS 中的私钥仍然可被摄像头上运行的代码读取;ROMFS 防的是修改,而不是提取。要替换一张存放于 ROMFS 中的证书,只能通过重新构建并重新刷写镜像来实现。

14.4.3.4. 使用证书

一个完整的客户端,它会设置时钟、打开套接字、验证一台自签名的服务器并交换数据:

import socket
import ssl
import ntptime

ntptime.settime()                 # correct clock for the validity check

# Open a plain TCP connection.
addr = socket.getaddrinfo("openmv", 8443)[0][-1]
sock = socket.socket()
sock.connect(addr)

# Wrap it for TLS, trusting the server's self-signed certificate.
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.load_verify_locations(cafile="server.der")
ssock = ctx.wrap_socket(sock, server_hostname="openmv")

ssock.write(b"hello\n")
print(ssock.read())
ssock.close()

一个完整的服务器,出示自己的证书和密钥:

import socket
import ssl
import ntptime

ntptime.settime()                 # correct clock for the validity check

ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain("server.der", "server.key.der")

sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(socket.getaddrinfo("0.0.0.0", 8443)[0][-1])
sock.listen(1)

while True:
    client, addr = sock.accept()
    sclient = ctx.wrap_socket(client, server_side=True)
    sclient.write(b"hello\n")
    print(sclient.read())
    sclient.close()

对于双向身份验证(mTLS),服务器还会额外要求并验证一张客户端证书,而客户端则出示自己的一张:

# Server side: also demand and verify a client certificate.
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain("server.der", "server.key.der")
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.load_verify_locations(cafile="client.der")

# Client side: present a certificate of our own.
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.load_cert_chain("client.der", "client.key.der")
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.load_verify_locations(cafile="server.der")

完整的 API 请参见 ssl 模块文档。

备注

本页中的所有内容都原封不动地适用于 DTLS(基于 UDP 的 TLS)。密钥、证书、DER 格式、信任模型、过期相关的考量以及 load_cert_chain / load_verify_locations 调用都是完全相同的;唯一不同的是传输层——你要包装一个 socket.SOCK_DGRAM 套接字,并选择 ssl.PROTOCOL_DTLS_CLIENT / ssl.PROTOCOL_DTLS_SERVER 来代替 TLS 协议常量。额外多出的一个小环节是服务器端的反欺骗 cookie——来自新客户端的第一次连接预期会失败,而客户端只需重试即可;详见 DTLS 支持