ssl --- SSL/TLS 模块

本模块为网络套接字提供传输层安全(Transport Layer Security,旧称且广为人知的“安全套接字层”)加密以及对端身份验证功能,客户端和服务器端均可使用。

小技巧

初次在摄像头上使用 TLS? 请从 使用 TLS 证书 教程开始。它会逐步讲解如何选择密钥类型、创建证书并将其转换为摄像头所需的 DER 格式、将证书部署到设备上,以及验证服务器和客户端——并附有完整的可运行示例。

备注

MicroPython 未实现 ssl.SSLError。SSL/TLS 失败会以 OSError 的形式抛出。

示例

TLS 客户端,使用存储在文件系统上的 CA 证书(DER 格式)来验证服务器的证书:

import socket
import ssl
import ntptime

# CERT_REQUIRED checks the certificate's validity dates, so the clock
# must be set (see the certificates tutorial linked above).
ntptime.settime()

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

# Wrap it for TLS and require a valid certificate.
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.load_verify_locations(cafile="ca.der")
ssock = ctx.wrap_socket(sock, server_hostname="example.com")

ssock.write(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
print(ssock.read())
ssock.close()

若只需要快速建立一个 不安全的 连接(不进行证书验证),可以改用 ssl.wrap_socket() 便捷函数:

ssock = ssl.wrap_socket(sock, server_hostname="example.com")

TLS 服务器,提供其自身的证书和私钥(DER 格式):

import socket
import ssl

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)

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

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

函数

ssl.wrap_socket(sock: Any, server_side: bool = False, key: bytes | None = None, cert: bytes | None = None, cert_reqs: int = CERT_NONE, cadata: bytes | None = None, server_hostname: str | None = None, do_handshake: bool = True) Any

包装给定的 sock 并返回一个新的已包装套接字对象。该函数的实现方式是先创建一个 SSLContext,然后在该上下文对象上调用 SSLContext.wrap_socket() 方法。参数 sockserver_sideserver_hostname 会原样传递给该方法调用。参数 do_handshake 会作为 do_handshake_on_connect 传递。其余参数的行为如下:

  • cert_reqs 决定对端(服务器或客户端)是否必须提供有效的证书。请注意 ssl.CERT_NONEssl.CERT_OPTIONAL 都不会验证任何证书;只有 ssl.CERT_REQUIRED 才会进行验证。

  • cadata 是一个 bytes 对象,包含将用于验证对端证书的 CA 证书链(DER 格式)。目前仅支持单个 DER 编码的证书。

class ssl.SSLContext(protocol: int, /)

创建一个新的 SSLContext 实例。protocol 参数必须是 PROTOCOL_* 常量之一。

load_cert_chain(certfile: str | bytes, keyfile: str | bytes) None

加载一个私钥及其对应的证书。certfile 是一个字符串,表示证书的文件路径。keyfile 是一个字符串,表示私钥的文件路径。

与 CPython 的区别

MicroPython 扩展:certfilekeyfile 可以是 bytes 对象而非字符串,在这种情况下它们会被解释为实际的证书/密钥数据。

load_verify_locations(cafile: str | None = None, cadata: bytes | None = None) None

加载将用于验证对端证书的 CA 证书链。cafile 是 CA 证书的文件路径。cadata 是一个包含 CA 证书的 bytes 对象。这两个参数只应提供其中之一。

get_ciphers() List[str]

获取已启用的密码套件列表,以字符串列表的形式返回。

set_ciphers(ciphers: List[str]) None

为使用该上下文创建的套接字设置可用的密码套件。ciphers 应为采用 IANA 密码套件格式 的字符串列表。

wrap_socket(sock: Any, *, server_side: bool = False, do_handshake_on_connect: bool = True, server_hostname: str | None = None, client_id: bytes | None = None) Any

接收一个 stream sock(通常是 SOCK_STREAM 类型的 socket.socket 实例),并返回一个 ssl.SSLSocket 实例,对底层流进行包装。返回的对象具有常见的 stream 接口方法,如 read()write() 等。

  • server_side 选择被包装的套接字位于服务器端还是客户端。服务器端的 SSL 套接字应当从非 SSL 监听服务器套接字上调用 accept() 所返回的普通套接字来创建。

  • do_handshake_on_connect 决定握手是作为 wrap_socket 的一部分立即完成,还是推迟到初次读写时再进行。对于阻塞套接字,立即握手是标准做法。对于非阻塞套接字(即传入 wrap_socketsock 处于非阻塞模式时),通常应推迟握手,否则 wrap_socket 会一直阻塞直到握手完成。

  • server_hostname 供客户端使用,用于设置要对照所接收到的服务器证书进行核对的主机名。它还会设置服务器名称指示(SNI)的名称,从而让服务器能够提供正确的证书。

  • client_id 是 MicroPython 特有的扩展参数,仅在实现 DTLS 服务器时使用。详见 DTLS 支持

警告

默认情况下不进行任何证书验证(ssl.CERT_NONE)。为建立安全连接,你必须通过将 cert_reqs / SSLContext.verify_mode 设置为 ssl.CERT_REQUIRED 来验证对端的证书;否则连接将容易受到中间人攻击。

CPython 的 wrap_socket 返回一个 SSLSocket 对象,它具有套接字常见的方法,如 sendrecv 等。MicroPython 的 wrap_socket 返回的对象更类似于 CPython 的 SSLObject,后者不具有这些套接字方法。

verify_mode

设置或获取对端证书的验证行为。必须是 CERT_* 常量之一。

备注

ssl.CERT_REQUIRED 要求设备的日期/时间设置正确,例如使用 mpremote rtc --setntptime,并且在客户端时必须指定 server_hostname

DTLS 支持

与 CPython 的区别

这是一项 MicroPython 扩展。

本模块通过 PROTOCOL_DTLS_CLIENTPROTOCOL_DTLS_SERVER 常量支持客户端和服务器模式的 DTLS,这些常量可用作 SSLContextprotocol 参数。

在这种情况下,底层套接字应当表现得像数据报套接字(即类似于以 socket.AF_INET 作为 af、以 socket.SOCK_DGRAM 作为 type 通过 socket.socket 打开的套接字)。

DTLS 服务器支持

MicroPython 的 DTLS 服务器支持按照 DTLS 1.2 的要求配置了“Hello Verify”。这对 DTLS 客户端是透明的,但在 MicroPython 中实现 DTLS 服务器时有一些需要注意的事项:

  • 服务器在调用 SSLContext.wrap_socket() 时应额外传入一个参数 client_id。该 ID 必须是一个 bytes 对象(或类似对象),其中包含表示该客户端的、与传输相关的标识符。

    最简单的做法是将 socket.recv_from() 返回的元组 (client_ip, client_port) 转换为字节串,即:

    _, client_addr = sock.recvfrom(1, socket.MSG_PEEK)
    sock.connect(client_addr)  # Connect back to the client
    sock = ssl_ctx.wrap_socket(sock, server_side=True,
                               client_id=repr(client_addr).encode())
    
  • 客户端首次连接时,服务器对 wrap_socket 的调用会失败并抛出 OSError 错误“Hello Verify Required”。这是因为客户端尚未获知 DTLS 的“Hello Verify”cookie。如果同一客户端再次连接,则 wrap_socket 将会成功。

  • 用于“Hello Verify”的 DTLS cookie 与 SSLContext 对象相关联,因此对于来自同一客户端的后续连接,应使用同一个 SSLContext 对象来进行包装。该 cookie 实现包含超时机制,且无论有多少客户端连接,内存占用都是恒定的,因此在服务器的整个生命周期内重复使用同一个 SSLContext 对象是没有问题的。

常量

ssl.PROTOCOL_TLS_CLIENT: int

protocol 参数支持的取值,用于选择 TLS 客户端模式。

ssl.PROTOCOL_TLS_SERVER: int

protocol 参数支持的取值,用于选择 TLS 服务器模式。

ssl.PROTOCOL_DTLS_CLIENT: int

protocol 参数支持的取值,用于选择 DTLS 客户端模式。

ssl.PROTOCOL_DTLS_SERVER: int

protocol 参数支持的取值,用于选择 DTLS 服务器模式。

ssl.CERT_NONE: int

cert_reqs 参数以及 SSLContext.verify_mode 属性支持的取值。不对对端进行证书验证。

ssl.CERT_OPTIONAL: int

cert_reqs 参数以及 SSLContext.verify_mode 属性支持的取值。证书验证是可选的。请注意,在 OpenMV Cam 上其行为与 ssl.CERT_NONE 相同。

ssl.CERT_REQUIRED: int

cert_reqs 参数以及 SSLContext.verify_mode 属性支持的取值。要求对端提供有效的证书。