Nhiều người nghĩ về HTTPS như một cái công tắc: bật lên là "an toàn", tắt đi là "không an toàn". Thực tế TLS có hai phần rất khác nhau. Phần thứ nhất là chi phí: bật TLS tốn thêm bao nhiêu thời gian cho mỗi kết nối? Phần thứ hai, quan trọng hơn nhiều, là cấu hình: cùng một chữ "HTTPS" trên thanh địa chỉ có thể là một máy chủ chỉ nói TLS 1.3 với cipher 256-bit, cũng có thể là một máy chủ vẫn chấp nhận TLS 1.0 và một chứng chỉ chẳng ai ký.
Bài này đo cả hai. Tôi dựng một máy chủ TLS thật bằng openssl s_server trong container dùng một lần, đo thời gian bắt tay, rồi thử ba kiểu cấu hình sai để xem cái nào bị chặn và cái nào lọt. Mỗi phép đo lặp 3 lần. Không nhắm vào hệ thống của ai — mọi thứ chạy trên localhost trong container tự dựng rồi tự xoá.
Phần 1: bắt tay TLS tốn bao nhiêu?
Tôi cho client mở kết nối TCP tới openssl s_server, bấm giờ đúng đoạn wrap_socket — tức là chỉ đo phần bắt tay TLS, không tính thời gian gửi dữ liệu. Lặp 20 lần rồi lấy trung vị, và cả bộ đo lặp 3 lần.
thoi gian bat tay TLS trung vi: 0.68 ms (lan 1)
0.93 ms (lan 2)
0.94 ms (lan 3)
giao thuc thuong luong: TLSv1.3
cipher: TLS_AES_256_GCM_SHA384 (256 bit)
Chưa tới một mili giây cho mỗi lần bắt tay, trên máy cùng nhà (client và server cùng container, không có độ trễ mạng). Con số này quan trọng vì nó bác bỏ cái lý do hay được viện ra để không bật HTTPS: "TLS làm chậm trang". Trên đường truyền thật sẽ có thêm độ trễ khứ hồi — TLS 1.3 cần một vòng khứ hồi để bắt tay, TLS 1.2 cần hai — nhưng chi phí tính toán của bản thân việc bắt tay thì nhỏ như trên.
Và đây là điểm mấu chốt của phép đo: bắt tay chỉ xảy ra một lần cho mỗi kết nối. Một trang web tải hàng chục tài nguyên qua cùng một kết nối (HTTP keep-alive, hay ghép luồng của HTTP/2) chỉ bắt tay một lần rồi dùng lại. Thêm cơ chế nối lại phiên (session resumption) thì lần sau còn rẻ hơn nữa. Nói cách khác: chi phí TLS là một khoản trả một lần, không phải khoản thu trên từng request. Ai đo throughput bằng cách mở kết nối mới cho mỗi request rồi kết luận "TLS đắt" là đang đo sai thứ.
Phần 2: máy chủ có từ chối giao thức cũ không?
Đây mới là phần dễ sai. Tôi cấu hình s_server chỉ bật TLS 1.2 và 1.3, rồi cho client cố tình ép từng phiên bản giao thức:
tls1 (TLS 1.0): no protocols available -> bi tu choi
tls1_1 (TLS 1.1): no protocols available -> bi tu choi
tls1_2 (TLS 1.2): bat tay duoc
tls1_3 (TLS 1.3): bat tay duoc
Kết quả này giống hệt qua cả 3 lần đo. TLS 1.0 và 1.1 bị từ chối thẳng với thông báo no protocols available — hai bên không tìm được phiên bản chung nào, bắt tay dừng ngay. Đó là hành vi đúng. TLS 1.0 (năm 1999) và 1.1 (2006) đã bị khai tử: chúng cho phép những cipher yếu và dính các đòn như BEAST, POODLE. Trình duyệt hiện đại đã bỏ hẳn từ 2020.
Nhưng "đúng" ở đây là nhờ tôi đã cấu hình đúng. Một máy chủ để nguyên cấu hình mặc định cũ, hoặc một người quản trị bật lại TLS 1.0 "cho tương thích với khách hàng dùng máy cũ", sẽ cho cái bắt tay đó lọt qua. Cái nguy hiểm là không có lỗi nào hiện ra: trang vẫn chạy, khoá vẫn xanh, chỉ có điều nó chấp nhận một cuộc thương lượng mà kẻ đứng giữa có thể ép tụt xuống mức yếu nhất. Cách duy nhất để biết là chủ động kiểm tra — đúng như phép đo này làm.
Muốn kiểm nhanh một máy chủ (của chính bạn), lệnh thủ công là:
# thu ep TLS 1.0 - phai bi tu choi
echo | openssl s_client -connect example.com:443 -tls1 2>&1 | grep -i protocol
Nếu nó bắt tay được, đó là một phát hiện cần sửa.
Phần 3: chứng chỉ tự ký — ai từ chối, ai lọt?
Phần cuối là về danh tính. TLS làm hai việc: mã hoá đường truyền, và chứng minh "tôi đúng là máy chủ bạn muốn nói chuyện". Việc thứ hai dựa vào chứng chỉ được một CA (tổ chức chứng thực) đáng tin ký. Chứng chỉ tự ký thì không có ai ký cả — nó mã hoá được, nhưng không chứng minh được danh tính.
Tôi cho client kết nối tới máy chủ dùng chứng chỉ tự ký, theo hai cách:
client mac dinh (kiem CA): TU CHOI - CERTIFICATE_VERIFY_FAILED
client TAT kiem tra: chap nhan (nhung mo cua MITM)
Client mặc định — trình duyệt, curl, thư viện HTTP chuẩn — kiểm chứng chỉ và từ chối cái tự ký với lỗi CERTIFICATE_VERIFY_FAILED. Đây chính là trang cảnh báo đỏ "Kết nối của bạn không riêng tư" mà ai cũng từng gặp. Nó khó chịu, nhưng nó đang làm đúng việc: nó không có cách nào biết chứng chỉ tự ký này là của máy chủ thật hay của kẻ đứng giữa.
Vế thứ hai mới là bài học đắt. Khi lập trình viên gặp lỗi chứng chỉ trong lúc phát triển, phản xạ thường thấy là tắt kiểm tra đi cho xong:
# Python
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
curl --insecure ... # hay -k
Đúng là hết lỗi thật. Kết nối vẫn được mã hoá. Nhưng bạn vừa vứt bỏ toàn bộ khả năng chống kẻ-đứng-giữa (MITM). Một kẻ chen được vào đường truyền — Wi-Fi công cộng, DNS bị bẻ, proxy độc hại — chỉ cần chìa ra chứng chỉ tự ký của chính hắn, và client đã tắt kiểm tra sẽ vui vẻ nhận, mã hoá dữ liệu bằng khoá của kẻ tấn công rồi gửi thẳng cho hắn. Mã hoá mà không xác minh danh tính là khoá cửa nhà rồi đưa chìa cho người lạ đứng ngoài tự nhận là chủ nhà.
Cái bẫy là dòng CERT_NONE đó hay bị viết trong lúc dev rồi theo bài lên production. Nó không gây lỗi, không có cảnh báo, test vẫn xanh — cho tới ngày có người thật sự đứng giữa. Nếu môi trường dev cần chứng chỉ riêng, cách đúng là thêm CA nội bộ vào danh sách tin cậy, chứ không phải tắt kiểm tra cho tất cả.
Ghép lại
Sơ đồ dưới đây gom ba phép đo: chi phí bắt tay ở cột trái, xử lý giao thức cũ ở giữa, xử lý chứng chỉ ở phải, và bài học chung ở dưới.
Ba con số, một mạch suy nghĩ:
- Bắt tay rẻ và trả một lần (~0,7–0,9 ms). Đừng lấy "TLS chậm" làm cớ — trừ khi bạn đo bằng cách mở kết nối mới cho mỗi request, nghĩa là đang tự làm hỏng phép đo.
- Giao thức cũ phải bị từ chối. TLS 1.0/1.1 lọt qua là một lỗ hổng im lặng; chỉ chủ động kiểm mới thấy.
- Kiểm chứng chỉ là phần không được tắt. Mã hoá bảo vệ nội dung khỏi bị đọc; xác minh chứng chỉ bảo vệ bạn khỏi nói chuyện nhầm người. Bỏ vế sau thì vế đầu thành vô nghĩa.
Điểm chung của cả Series này lại đúng ở đây: cơ chế mặc định của công cụ hiện đại thường an toàn (client mặc định từ chối chứng chỉ tự ký, s_server mới không bật TLS 1.0). Lỗ hổng gần như luôn đến từ chỗ có người chủ động tắt cái bảo vệ đó — CERT_NONE, --insecure, bật lại giao thức cũ "cho tương thích". An toàn không phải là thêm một lớp; nó là không gỡ lớp đã có sẵn.
Thử ba mươi giây
Mở terminal, trỏ vào một máy chủ của chính bạn và chạy:
# 1. Xem no thuong luong giao thuc va cipher gi
echo | openssl s_client -connect your-server:443 2>/dev/null | grep -iE "protocol|cipher"
# 2. Thu ep TLS 1.0 - phai thay 'no protocols available'
echo | openssl s_client -connect your-server:443 -tls1 2>&1 | grep -iE "protocol|no protocols"
Nếu dòng đầu ra TLSv1.3 với một cipher GCM, và dòng sau báo no protocols available — máy chủ của bạn đang làm đúng. Nếu TLS 1.0 bắt tay được, bạn vừa tìm ra việc cần làm chiều nay.