Hai phần đầu đo TLS như một bài toán hiệu năng: phần 1 đọc từng byte của một bắt tay, phần 2 nhân nó lên mười nghìn. Nhưng trong vận hành, TLS hiếm khi gãy vì chậm. Nó gãy lúc 3 giờ sáng vì chứng chỉ hết hạn, gãy sau một lần deploy vì ai đó copy file leaf mà quên file intermediate, gãy trên một máy duy nhất vì đồng hồ máy đó lệch. Mọi sự cố ấy đều nằm ở bước client kiểm tra Certificate mà server gửi, và thông điệp lỗi của bước đó, với Go, với curl, với OpenSSL, là thứ bạn sẽ đọc trong log khi sự cố xảy ra. Bài này tự dựng một CA bằng crypto/x509, cấp chứng chỉ đúng, rồi cố tình làm hỏng nó theo năm cách, phục vụ mỗi biến thể trên một cổng, và cho ba client đọc. Mục tiêu là lúc gặp dòng x509: certificate signed by unknown authority trong log, bạn biết nó có thể là ba chuyện khác nhau.
Client kiểm tra gì khi nhận Certificate

Hình 1: Năm phép kiểm tra client làm với chuỗi chứng chỉ, bảy biến thể được phục vụ trên bảy cổng, và cách đo.
Khi server gửi thông điệp Certificate, client làm năm việc, theo thứ tự này trong crypto/x509. Chuỗi: leaf phải do một CA ký, CA đó phải do một CA khác ký, cho tới khi gặp một root nằm trong kho tin cậy của client. Server phải gửi leaf và các intermediate; root thì client đã có sẵn, gửi thêm chỉ tốn byte. Thời gian: thời điểm hiện tại phải nằm trong khoảng NotBefore và NotAfter của mọi chứng chỉ trong chuỗi, và "hiện tại" là theo đồng hồ của client, không phải của server. Tên: tên client định nối, ServerName trong Go hay host trong URL với curl, phải khớp một mục trong Subject Alternative Name, DNS hoặc IP; CommonName đã bị bỏ qua từ Go 1.15 và Chrome 58. Mục đích: leaf phải có ExtKeyUsage serverAuth, CA phải có BasicConstraints CA=true và KeyUsage certSign, để một leaf không thể tự ký leaf khác. Chữ ký: mỗi mắt xích được xác minh bằng khoá công khai của mắt xích trên, đây là phần tốn CPU và phụ thuộc thuật toán.
Bộ chứng chỉ sinh bằng Go, không dùng openssl, để thấy mỗi thuộc tính nằm ở trường nào của x509.Certificate. Một root ECDSA P-256 sống 10 năm, một intermediate 5 năm, và các leaf 90 ngày cho tên tls-lab, api.tls-lab và IP 172.23.0.6. Từ đó tạo bảy biến thể: good.pem là leaf cộng intermediate, noint.pem chỉ có leaf, badsan.pem cấp cho khac.example, expired.pem có NotAfter là hôm qua, future.pem có NotBefore là ngày mai, self.pem tự ký không qua CA, và rsa-good.pem là một chuỗi RSA riêng với root 4096 bit, intermediate và leaf 2048 bit, ký bằng root RSA riêng. Mỗi biến thể chạy một server TLS echo trên dist-lab, cổng 9460 đến 9466. Client Go trên go-lab với RootCAs là root.pem và ServerName là tls-lab nối từng cổng 20 lần, in OK kèm số chứng chỉ, tổng byte DER server gửi và p50 bắt tay, hoặc in nguyên văn lỗi. Rồi curl với --cacert root.pem và openssl s_client -CAfile root.pem làm lại để so thông điệp. Lệch giờ được mô phỏng bằng tls.Config.Time, trả về thời gian hiện tại cộng 48 giờ, không đụng đồng hồ máy.
Đo thật: 5 cách gãy, 3 cách báo lỗi, và một cái OK không nên có

Hình 2: Thông điệp lỗi của Go, curl và OpenSSL cho từng biến thể, cái OK sai khi client lệch giờ, và kích thước chuỗi ECDSA so với RSA.
| biến thể (cổng) | Go crypto/x509 |
curl ssl_verify |
|---|---|---|
| đúng (9460) | OK, 2 chứng chỉ, 860 B DER, p50 0,54 ms | 0 |
| thiếu intermediate (9461) | certificate signed by unknown authority |
20 |
| SAN sai (9462) | certificate is valid for khac.example, not tls-lab |
1 |
| hết hạn hôm qua (9463) | has expired or is not yet valid: … is after … |
10 |
| chưa hiệu lực (9464) | … is before 2026-11-09T12:45:44Z |
9 |
| chưa hiệu lực, client +2 ngày | OK, 848 B | — |
| tự ký (9465) | certificate signed by unknown authority |
18 |
| đúng, verify bằng root RSA | certificate signed by unknown authority |
20 |
| chuỗi RSA (9466) | OK, 1.908 B DER, p50 1,34 ms | 0 |
Ba client cùng chặn đủ năm biến thể hỏng, không cái nào lọt. Đó là tin tốt và là điều nên nói trước: với RootCAs đặt đúng và ServerName đặt đúng, crypto/tls mặc định không cho qua bất kỳ chứng chỉ nào thiếu intermediate, sai tên, hết hạn, chưa hiệu lực hay tự ký, và curl với OpenSSL cũng vậy. Mọi sự cố "chứng chỉ sai mà vẫn chạy" trong thực tế đều do ai đó đã bật InsecureSkipVerify, thêm -k vào curl, hoặc nhét chứng chỉ tự ký vào kho tin cậy của máy. Chuỗi đúng cho kết quả gọn: hai chứng chỉ, leaf 425 byte và intermediate 435 byte, cộng lại 860 byte DER, và bắt tay đầy đủ p50 0,54 ms trên mạng Docker nội bộ.
Go báo ba lỗi khác nhau bằng cùng một câu. Thiếu intermediate, tự ký, và chứng chỉ hoàn toàn đúng nhưng client cầm nhầm root, cả ba đều ra x509: certificate signed by unknown authority. Về mặt thuật toán thì đúng: trong cả ba trường hợp, Verify đi từ leaf lên và không tìm thấy mắt xích nào dẫn tới một root nó tin, nên nó không phân biệt được "server quên gửi" với "client cầm sai root" với "chẳng có CA nào cả". Nhưng với người đọc log, ba chuyện đó dẫn tới ba việc sửa khác nhau: sửa file trên server, sửa file trên client, hoặc chấp nhận rằng đây là môi trường dev. OpenSSL tách được một phần: tự ký cho mã 18 self-signed certificate, nhờ phát hiện issuer trùng subject, nhưng thiếu intermediate và sai root đều cho mã 20 unable to get local issuer certificate. openssl s_client còn in thêm mã 21 unable to verify the first certificate ngay sau, vì nó tiếp tục quá trình sau lỗi đầu. Vậy khi gặp unknown authority, bước đầu tiên là mở chuỗi server gửi ra xem có bao nhiêu chứng chỉ, bằng openssl s_client -showcerts; nếu chỉ có một, lỗi nằm ở server.
Lỗi tên và lỗi thời gian thì Go nói rõ, kèm cả số liệu để sửa. Sai SAN: certificate is valid for khac.example, not tls-lab, cho biết cả tên trong chứng chỉ lẫn tên client đang tìm. Hết hạn: current time 2026-11-08T12:46:22Z is after 2026-11-07T12:45:44Z, in cả hai mốc thời gian nên nhìn là biết hết hạn được bao lâu. Chưa hiệu lực dùng cùng câu mở đầu has expired or is not yet valid nhưng đuôi là is before, nên muốn phân biệt hai trường hợp phải đọc hết dòng. curl gọn hơn: certificate has expired mã 10, certificate is not yet valid mã 9, no alternative certificate subject name matches target host name 'tls-lab' mã 1. Cái tên "mã 1" của curl đáng lưu ý vì OpenSSL dùng mã 1 cho lỗi chung, curl mượn nó cho lỗi khớp tên do chính curl kiểm tra chứ không phải OpenSSL. Nối bằng IP 172.23.0.6 thay vì tên thì OK, vì leaf có IP Address:172.23.0.6 trong SAN; nếu thiếu mục đó, chứng chỉ "đúng" vẫn bị từ chối khi client nối thẳng bằng IP, chuyện hay gặp với health check nội bộ.
Lệch giờ hai ngày biến chứng chỉ chưa hiệu lực thành hợp lệ. Chứng chỉ trên cổng 9464 bị từ chối vì NotBefore là ngày mai; chỉ cần tls.Config.Time trả về hiện tại cộng 48 giờ, client chấp nhận nó, OK với 848 byte. Điều này không phải lỗi của Go, nó đúng theo đặc tả: thời gian được kiểm theo đồng hồ của client. Hệ quả vận hành có hai chiều. Chiều một là máy client lệch giờ sẽ từ chối chứng chỉ hoàn toàn đúng, đặc biệt chứng chỉ Let's Encrypt vừa gia hạn vì NotBefore của chúng thường chỉ lùi một giờ so với lúc cấp, và thiết bị nhúng với đồng hồ reset về 1970 sẽ không nối được với bất kỳ server HTTPS nào. Chiều hai là máy client lệch giờ sẽ chấp nhận chứng chỉ đã hết hạn, kể cả chứng chỉ bị lộ khoá mà người ta chỉ chờ nó hết hạn chứ không thu hồi. Đồng hồ đúng là một phần của mô hình an toàn TLS, và NTP không phải việc phụ.
Chuỗi RSA nặng gấp 2,2 lần và bắt tay chậm gấp 2,5 lần. Root RSA-4096 1.310 byte, intermediate RSA-2048 1.095 byte, leaf RSA-2048 813 byte, so với 394, 435 và 425 byte của ECDSA P-256. Server gửi leaf cộng intermediate: 1.908 byte RSA so với 860 byte ECDSA. Bắt tay p50 1,34 ms so với 0,54 ms, chênh 0,8 ms, trong đó có chữ ký RSA-2048 mà phần 2 đo khoảng 730 µs CPU phía server, cộng với client phải verify hai chữ ký RSA trên chuỗi thay vì hai chữ ký ECDSA. Verify RSA rẻ hơn ký RSA nhiều, nên khoản này nhỏ, nhưng kích thước thì không nhỏ: với CA công cộng, chuỗi RSA thật thường còn có root cross-sign và intermediate dài hơn, dễ vượt 4 KB, tức là Certificate không còn nằm gọn trong một segment TCP, và đó là lý do phần 1 thấy server flight bị tách gói.
Đánh đổi cần cân nhắc
Luôn gửi đủ intermediate, và kiểm bằng client không có cache. Trình duyệt thường tự tải intermediate còn thiếu qua trường Authority Information Access, hoặc đã có sẵn từ lần ghé một site khác dùng cùng CA, nên server thiếu intermediate vẫn "chạy được" trên máy của người deploy. Go, curl và mọi client lập trình thì không. Hãy test bằng openssl s_client -showcerts hoặc một client Go từ máy sạch; thấy một chứng chỉ thay vì hai là thiếu.
Cấp cho SAN đủ tên, kể cả IP nếu có ai nối bằng IP. CommonName không còn giá trị với bất kỳ client hiện đại nào. Health check nội bộ, sidecar, và script bảo trì thường nối bằng IP; hoặc cấp SAN có IP, hoặc bắt chúng nối bằng tên. InsecureSkipVerify cho health check là cách đi tắt phổ biến và là cách mà một chứng chỉ hết hạn không bao giờ được phát hiện trước khi người dùng thật gặp.
Phạm vi đo. Không có thu hồi: CRL và OCSP là chủ đề riêng, và Go mặc định không kiểm tra thu hồi, phần 7 sẽ bàn. Thời gian bắt tay 0,54 và 1,34 ms là trên mạng Docker không độ trễ, phần chữ ký là phần thay đổi, phần còn lại sẽ bị RTT lấn át trên mạng thật. CA ở đây tự dựng; với CA công cộng còn có Certificate Transparency và chính sách của CA/Browser Forum như giới hạn 398 ngày, những ràng buộc mà crypto/x509 không kiểm nhưng trình duyệt thì có.
Ba ý mang về
unknown authoritylà ba lỗi khác nhau: thiếu intermediate trên server, sai root trên client, hoặc tự ký. Mở chuỗi bằngopenssl s_client -showcertstrước khi sửa; OpenSSL tách tự ký thành mã 18, còn hai cái kia đều là 20.- Thời gian được kiểm theo đồng hồ client: lệch 2 ngày là chứng chỉ chưa hiệu lực được chấp nhận và chứng chỉ đúng bị từ chối. NTP là một phần của bảo mật TLS, không phải tiện ích.
- ECDSA P-256 cho chuỗi 860 byte và bắt tay 0,54 ms, RSA cho 1.908 byte và 1,34 ms: ba client đều chặn đủ năm lỗi, nên khác biệt còn lại giữa hai loại chứng chỉ là kích thước và CPU, và cả hai đều nghiêng về ECDSA.
Nguồn
- Go
crypto/x509—Certificate.Verify,VerifyOptions, và các kiểu lỗiUnknownAuthorityError,HostnameError,CertificateInvalidError: https://pkg.go.dev/crypto/x509 - Go
crypto/tls—Config.RootCAs,ServerName,Time: https://pkg.go.dev/crypto/tls#Config - RFC 5280 — Internet X.509 PKI Certificate and CRL Profile, mục 6.1 Basic Path Validation: https://www.rfc-editor.org/rfc/rfc5280#section-6.1
- RFC 6125 — Representation and Verification of Domain-Based Application Service Identity: https://www.rfc-editor.org/rfc/rfc6125
- OpenSSL
verify(1)— danh sách mã lỗi 9, 10, 18, 20, 21: https://docs.openssl.org/3.0/man1/openssl-verify/ - curl —
CURLINFO_SSL_VERIFYRESULTvà biến%{ssl_verify_result}: https://curl.se/libcurl/c/CURLINFO_SSL_VERIFYRESULT.html
Phần sau quay lại bên trong kết nối sau khi bắt tay xong: cipher suite. AES-128-GCM, AES-256-GCM và ChaCha20-Poly1305 trên máy có và không có lệnh AES phần cứng, tốc độ mã hoá dữ liệu thật tính bằng MB/s và chi phí trên mỗi record, để biết con số TLS "làm chậm" bao nhiêu khi truyền dữ liệu chứ không chỉ lúc mở kết nối.