14.4.3. 自簽憑證

自簽憑證是在兩台你所掌控的裝置之間讓 TLS 運作起來的最快方式:兩端都信任一張由你自己產生的憑證。它涵蓋了所有由你自行設定連線雙方的部署情境,唯有當第三方用戶端必須在未被告知要信任某張自訂憑證的情況下連線時,公開的憑證授權單位(Certificate Authority)才會登場。

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 :。在進行驗證的那一側,也請以 DER 形式複製 CA/對端憑證。

DER 檔案不一定要存放在可寫入的檔案系統上。MicroPython 也可以在 /rom 掛載一份唯讀的 ROMFS 映像,放在那裡的憑證載入方式與其他任何檔案完全相同,例如 ctx.load_cert_chain("/rom/server.der", "/rom/server.key.der")。ROMFS 映像是在你的開發機器上準備的,且在執行階段為唯讀,因此憑證無法在裝置上被竄改,這對於鎖定一台量產裝置很有用。請注意,存放在 ROMFS 中的私鑰仍可被相機上執行的程式碼讀取;ROMFS 防範的是修改,而非擷取。位於 ROMFS 中的憑證只能透過重新建置並重新燒錄映像來更換。

14.4.3.4. 使用憑證

一個完整的用戶端範例,會設定時鐘、開啟 socket、驗證一個自簽伺服器並交換資料:

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 socket,並選用 ssl.PROTOCOL_DTLS_CLIENT / ssl.PROTOCOL_DTLS_SERVER,而非 TLS 協定常數。唯一額外的小細節是伺服器端的防偽 cookie,新用戶端的第一次連線預期會失敗,用戶端只要重試即可;詳情請見 DTLS 支援