ssl --- SSL/TLS 模組

本模組為網路 socket 提供傳輸層安全性(Transport Layer Security,先前及廣為人知的名稱為「安全通訊端層」,Secure Sockets Layer)的加密與對端驗證功能,同時支援用戶端與伺服器端。

小訣竅

初次在相機上使用 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 並回傳一個新的已包裝 socket 物件。此函式的實作方式是先建立一個 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

設定使用此情境所建立的 socket 可用的密碼套件。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 選擇被包裝的 socket 是位於伺服器端還是用戶端。伺服器端的 SSL socket 應由非 SSL 監聽伺服器 socket 上 accept() 所回傳的一般 socket 建立而成。

  • do_handshake_on_connect 決定握手是作為 wrap_socket 的一部分完成,還是延後到作為初始讀取或寫入的一部分時才完成。對於阻塞式 socket,立即進行握手是標準做法。對於非阻塞式 socket(亦即傳入 wrap_socketsock 處於非阻塞模式時),通常應延後握手,否則 wrap_socket 會一直阻塞直到握手完成。

  • server_hostname 供用戶端使用,設定用以比對所收到伺服器憑證的主機名稱。它同時也設定伺服器名稱指示(Server Name Indication,SNI)所用的名稱,使伺服器能夠提供正確的憑證。

  • client_id 是 MicroPython 特有的擴充引數,僅在實作 DTLS 伺服器時使用。詳情請見 DTLS 支援

警告

預設情況下不會執行任何憑證驗證(ssl.CERT_NONE)。若要建立安全連線,您必須將 cert_reqsSSLContext.verify_mode 設定為 ssl.CERT_REQUIRED 來驗證對端的憑證;否則連線將容易遭受中間人攻擊。

CPython 的 wrap_socket 會回傳一個 SSLSocket 物件,該物件具有 socket 常見的方法,例如 sendrecv 等。而 MicroPython 的 wrap_socket 回傳的物件比較類似 CPython 的 SSLObject,它並不具備這些 socket 方法。

verify_mode

設定或取得對端憑證驗證的行為。必須為其中一個 CERT_* 常數。

備註

ssl.CERT_REQUIRED 要求裝置的日期/時間已正確設定,例如使用 mpremote rtc --setntptime,且在用戶端時必須指定 server_hostname

DTLS 支援

與 CPython 的差異

這是 MicroPython 的擴充功能。

本模組透過 PROTOCOL_DTLS_CLIENTPROTOCOL_DTLS_SERVER 常數支援用戶端與伺服器模式的 DTLS,這些常數可作為 SSLContextprotocol 引數使用。

在此情況下,底層 socket 應表現為資料包 socket(亦即如同以 socket.socket 開啟、其 afsocket.AF_INETtypesocket.SOCK_DGRAM 的 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 屬性的支援值。要求對端提供有效的憑證。