14.4.5. Xác minh máy chủ công cộng (camera đóng vai trò client)

Mọi điều đã đề cập ở trang trước về việc client "đã có root" đều đúng với trình duyệt, điện thoại và máy tính -- nhưng không đúng với camera. ssl của MicroPython không đi kèm kho tin tưởng tích hợp: một camera mới được nạp firmware không tin tưởng bất kỳ CA nào, và cấu hình mặc định (ssl.CERT_NONE) không xác minh gì cả, hoàn toàn dễ bị tấn công man-in-the-middle. Vì vậy khi camera là client kết nối ra ngoài tới một máy chủ TLS công cộng (API HTTPS, MQTT broker, ...) và bạn muốn nó thực sự xác minh máy chủ đó, bạn phải tự cung cấp điểm neo tin tưởng.

Cơ chế hoạt động giống hệt ví dụ client tự ký trên trang Chứng chỉ tự ký; điểm khác biệt duy nhất là tệp bạn tải là chứng chỉ CA thực sự thay vì chứng chỉ riêng của peer:

  1. Lấy chứng chỉ CA làm điểm neo cho chuỗi chứng chỉ của máy chủ. "Điểm neo" nghĩa là chứng chỉ ở (hoặc gần) đỉnh của chuỗi chứng chỉ máy chủ mà bạn chọn làm điểm bắt đầu của sự tin tưởng. Một máy chủ TLS gửi chứng chỉ lá và thường cả chứng chỉ trung gian; nó không bao giờ gửi root. Bạn phải tự lấy điểm neo tin tưởng đó độc lập với máy chủ -- việc tin tưởng bất kỳ thứ gì máy chủ gửi cho bạn sẽ làm mất hoàn toàn ý nghĩa của việc xác minh.

    Đầu tiên hãy tìm hiểu CA nào đã cấp chứng chỉ của máy chủ. Ví dụ, với openmv.io

    openssl s_client -connect openmv.io:443 -showcerts < /dev/null
    

    Khối Certificate chain liệt kê mỗi chứng chỉ với subject (s:) và issuer (i:); các phiên bản OpenSSL mới hơn cũng in a: (loại khóa) và v: (thời hạn) mà bạn có thể bỏ qua ở đây:

    Certificate chain
     0 s:CN=openmv.io
       i:C=US, O=Let's Encrypt, CN=E8
     1 s:C=US, O=Let's Encrypt, CN=E8
       i:C=US, O=Internet Security Research Group, CN=ISRG Root X1
    

    Entry 0 là chứng chỉ lá (openmv.io), được cấp bởi intermediate E8. Entry 1 là intermediate đó, được cấp bởi root ISRG Root X1. Issuer (i:) của entry cao nhất chính là tên root -- ở đây là ISRG Root X1. (Intermediate là E8 thay vì R10 / R11 mà bạn có thể đã thấy ở nơi khác vì openmv.io sử dụng chứng chỉ ECDSA; Let's Encrypt ký các chứng chỉ lá ECDSA bằng các intermediate series E và ký các chứng chỉ lá RSA bằng series R. Cả hai đều chuỗi lên ISRG Root X1.)

    OpenSSL cũng in các dòng depth= và có thể báo cáo root với Verification: OK. Điều đó xảy ra chỉ vì PC của bạn đã tin tưởng ISRG Root X1 -- máy chủ đã không gửi nó (máy chủ không bao giờ gửi root của mình), và camera, vì không có kho tin tưởng, cũng sẽ không có nó. Đó chính xác là lý do tại sao bạn phải tự cung cấp nó.

    Tải xuống root đó từ các root được CA công bố. Let's Encrypt liệt kê tất cả của họ trên trang Let's Encrypt certificates page; tệp trực tiếp cho ISRG Root X1 là isrgrootx1.pem (họ cũng cung cấp dưới dạng đã mã hóa sẵn isrgrootx1.der). Các CA khác công bố của họ trên trang "root certificates" / "repository" tương tự; bộ công cộng chuẩn là Mozilla CA program (CCADB). Xác nhận bạn đã tải đúng tệp bằng cách so sánh fingerprint của nó với giá trị CA công bố (thêm -inform DER nếu bạn tải tệp .der):

    openssl x509 -in isrgrootx1.pem -noout -subject -fingerprint -sha256
    

    Nếu bạn không muốn theo dõi một root, thay vào đó bạn có thể sao chép intermediate trực tiếp từ đầu ra -showcerts (khối -----BEGIN CERTIFICATE----- thứ hai), tin tưởng nó, và chấp nhận rằng bạn phải làm mới nó mỗi khi CA luân chuyển intermediate -- thường xuyên hơn nhiều so với root (xem sự đánh đổi bên dưới).

  2. Chuyển đổi sang DER, chính xác như trước:

    openssl x509 -in isrgrootx1.pem -outform DER -out ca.der
    
  3. Sao chép ca.der vào camera (filesystem hoặc ROMFS) và tải nó làm điểm neo tin tưởng:

    import socket
    import ssl
    import ntptime
    
    ntptime.settime()                  # validity check needs the clock
    
    addr = socket.getaddrinfo("api.example.com", 443)[0][-1]
    sock = socket.socket()
    sock.connect(addr)
    
    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="api.example.com")
    

    server_hostname là bắt buộc ở đây: nó điều khiển SNI và là tên được kiểm tra đối chiếu với subjectAltName của chứng chỉ máy chủ.

Mẹo

Phím tắt cho trường hợp thông thường. Let's Encrypt là CA công cộng được sử dụng rộng rãi nhất, và cả chứng chỉ RSA lẫn ECDSA của họ hiện tại đều chuỗi lên ISRG Root X1 (như ví dụ openmv.io ở trên cho thấy). Nếu các máy chủ camera của bạn kết nối tới sử dụng Let's Encrypt, bạn có thể bỏ qua hoàn toàn việc kiểm tra: chỉ cần đặt isrgrootx1.der vào camera và load_verify_locations nó.

Điều này không làm cho TLS hoạt động với mọi trang web. Một máy chủ có chứng chỉ từ CA khác (DigiCert, Google Trust Services, Amazon, Sectigo, ...) vẫn sẽ thất bại trong việc xác minh, và vì camera chỉ tin tưởng một chứng chỉ DER duy nhất trên mỗi ssl.SSLContext nên bạn không thể gộp mọi root như trình duyệt. Khi không chắc chắn, hãy xác định CA thực sự của máy chủ như đã chỉ dẫn ở trên và tin tưởng root đó.

Chứng chỉ nào bạn tin tưởng là một sự đánh đổi:

  • Root (được khuyến nghị). Tồn tại lâu dài -- thường hàng thập kỷ -- nên ca.der hiếm khi thay đổi. Nó yêu cầu máy chủ gửi intermediate để mbedTLS có thể xây dựng đường dẫn lá → intermediate → root tin tưởng của bạn; hầu hết mọi máy chủ công cộng được cấu hình đúng đều làm vậy.

  • Intermediate. Cũng hoạt động được, và vẫn hoạt động ngay cả khi máy chủ bỏ qua intermediate, nhưng các intermediate được luân chuyển thường xuyên hơn nhiều so với root, vì vậy bạn sẽ phải làm mới ca.der thường xuyên hơn.

  • Chính chứng chỉ lá (certificate pinning). Chặt chẽ nhất, nhưng chứng chỉ lá thay đổi mỗi lần gia hạn -- khoảng mỗi 90 ngày với Let's Encrypt -- vì vậy điều này chỉ hợp lý khi bạn cũng kiểm soát máy chủ và có thể đẩy pin mới đến mọi camera đồng bộ. Đây chính xác là điều mà ví dụ client tự ký thực hiện.

Ghi chú

ssl.SSLContext.load_verify_locations() nhận một chứng chỉ CA mã hóa DER duy nhất, vì vậy camera chỉ tin tưởng đúng một điểm neo tại một thời điểm. Để tiếp cận các máy chủ dưới các CA khác nhau, hãy sử dụng một ssl.SSLContext riêng cho mỗi điểm neo. Và vì chứng chỉ đó cuối cùng sẽ hết hạn hoặc bị CA luân chuyển, hãy xử lý nó như bất kỳ chứng chỉ nào khác trên thiết bị.