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

Ảnh chụp đoạn mã nền tối chứng chỉ và chuỗi tin cậy tự dựng CA rồi làm hỏng từng thứ với Go 1.23 crypto x509 và crypto tls curl và OpenSSL 3.0 CA gốc tới CA trung gian tới leaf ECDSA P-256 dist-lab cổng 9460 tới 9466, client kiểm tra gì khi nhận Certificate gồm chuỗi leaf do intermediate ký intermediate do một root trong kho tin cậy của client ký server gửi leaf cộng intermediate root client đã có thiếu intermediate là không nối được, thời gian now nằm trong NotBefore NotAfter của mọi chứng chỉ trong chuỗi theo đồng hồ của client, tên ServerName SNI Host phải khớp một mục trong SAN DNS hoặc IP CommonName không còn được dùng, mục đích leaf có ExtKeyUsage serverAuth CA có BasicConstraints CA true và KeyUsage certSign, chữ ký mỗi mắt xích verify chữ ký bằng khoá công khai của mắt xích trên ECDSA rẻ RSA-4096 đắt, bộ chứng chỉ sinh bằng Go gồm root.pem Lab Root CA 10 năm client tin, good.pem leaf tls-lab cộng intermediate 9460 đúng, noint.pem chỉ leaf thiếu intermediate 9461 lỗi kinh điển khi copy nhầm file, badsan.pem leaf cho khac.example 9462 cert hợp lệ nhưng không phải cho host này, expired.pem hết hạn hôm qua 9463 sự cố 3 giờ sáng, future.pem hiệu lực từ ngày mai 9464 lệch giờ client cộng 2 ngày thì lại OK, self.pem tự ký không qua CA 9465 dev local, rsa-good.pem chuỗi RSA root 4096 inter và leaf 2048 9466 so cỡ và thời gian verify với ECDSA, đo gì client Go RootCAs root.pem ServerName tls-lab nối từng cổng 20 lần OK kèm số cert byte DER bắt tay p50 hoặc lỗi nguyên văn của crypto x509 cùng các cổng bằng curl cacert root.pem và openssl s_client để so thông điệp lỗi lệch giờ mô phỏng bằng tls.Config.Time client cộng 2 ngày không chỉnh đồng hồ máy

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ó

Ảnh chụp output thật nền tối 7 cổng 1 chuỗi đúng 5 cách gãy 3 client với server dist-lab 9460 tới 9466 client go-lab Go 1.23 curl 7.88 OpenSSL 3.0.16 RootCAs root.pem, client Go crypto x509 ServerName tls-lab 20 lần bắt tay mỗi case gồm 9460 leaf cộng inter SAN tls-lab OK chuỗi 2 cert 860 byte DER bắt tay p50 0,54 ms, 9461 thiếu intermediate x509 certificate signed by unknown authority, 9462 SAN khac.example x509 certificate is valid for khac.example not tls-lab, 9463 hết hạn hôm qua x509 certificate has expired or is not yet valid current time 2026-11-08T12:46:22Z is after 2026-11-07T12:45:44Z, 9464 hiệu lực từ ngày mai current time is before 2026-11-09T12:45:44Z, 9464 client lệch cộng 2 ngày OK 848 byte DER p50 0,56 ms lệch giờ biến cert hỏng thành đúng, 9465 tự ký x509 certificate signed by unknown authority cùng chữ với 9461, 9460 verify bằng rsa-root.pem x509 certificate signed by unknown authority cert tốt root sai, 9460 nối bằng IP 172.23.0.6 OK SAN có IP Address, 9466 chuỗi RSA root 4096 OK chuỗi 2 cert 1908 byte DER bắt tay p50 1,34 ms gấp 2,5 ECDSA, cùng 7 cổng với curl cacert root.pem mã ssl_verify bằng mã OpenSSL gồm 9460 bắt tay xong báo HTTP 0.9 vì server là echo 0, 9461 unable to get local issuer certificate 20, 9462 no alternative certificate subject name matches tls-lab 1, 9463 certificate has expired 10, 9464 certificate is not yet valid 9, 9465 self-signed certificate 18, 9460 với cacert rsa-root.pem unable to get local issuer certificate 20 sai root bằng thiếu inter, openssl s_client 9461 verify error num 20 rồi num 21 unable to verify the first certificate, openssl s_client 9463 Verify return code 10 certificate has expired, kích thước chuỗi ECDSA P-256 so với RSA DER root 394 inter 435 leaf 425 server gửi 860 byte PEM good.pem 1275 noint.pem 631, RSA 4096 2048 root 1310 inter 1095 leaf 813 server gửi 1908 byte rsa-good.pem 2692 self.pem 530, leaf tls-lab notBefore Nov 8 11:45 2026 notAfter Feb 6 12:45 2027 90 ngày SAN DNS tls-lab DNS api.tls-lab IP Address 172.23.0.6, nói thẳng ba client cùng chặn đủ 5 biến thể hỏng nhưng chữ khác nhau Go gộp sai root thiếu intermediate và tự ký vào một câu OpenSSL tách tự ký 18 gộp hai cái kia 20 đọc lỗi xong vẫn phải mở cert ra xem bằng openssl x509 text

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ề

  1. unknown authority là 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ằng openssl s_client -showcerts trước khi sửa; OpenSSL tách tự ký thành mã 18, còn hai cái kia đều là 20.
  2. 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.
  3. 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

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.