Ba phần trước đo TLS theo lát cắt vận hành: bắt tay, mười nghìn kết nối, chuỗi chứng chỉ. Phần này đi vào ba quyết định mật mã nằm sau một cái tên như TLS_AES_128_GCM_SHA256: đổi khoá bằng đường cong nào, ký bằng khoá gì, và mã hoá dữ liệu bằng thuật toán nào. Ba quyết định đó tốn CPU ở ba chỗ khác nhau, một lần mỗi kết nối cho hai cái đầu, một lần mỗi byte cho cái cuối, và câu trả lời "cái nào nhanh" cho cái cuối đảo ngược hoàn toàn tuỳ CPU có lệnh AES hay không. Bài này đo cả ba với Go 1.23 trên máy aarch64 có lệnh AES, rồi tắt lệnh đó đi bằng một biến môi trường để thấy cùng một binary trên một CPU rẻ sẽ ra sao.
Một cipher suite là ba lựa chọn độc lập

Hình 1: Ba thành phần của một cipher suite, chỗ mỗi thành phần tốn CPU, và ba phép đo A, B, C.
Trong TLS 1.3, tên cipher suite chỉ còn nói về AEAD và hàm băm; key exchange và chữ ký được thương lượng riêng. Key exchange là phép Diffie-Hellman mà cả hai phía làm một lần mỗi kết nối mới: X25519, P-256, P-384, P-521, hoặc trong Go 1.23 mặc định là X25519Kyber768, bản lai cộng thêm một trao đổi hậu lượng tử. TLS 1.2 còn cho phép RSA key exchange, client mã hoá secret bằng khoá công khai trong chứng chỉ server, không có forward secrecy, và Go 1.22 trở đi tắt nó mặc định. Chữ ký phụ thuộc chứng chỉ server, ECDSA P-256 hay RSA-2048, chỉ server trả, phần 2 đã đo chênh lệch 730 µs. AEAD là thuật toán mã hoá dữ liệu sau bắt tay: AES-128-GCM, AES-256-GCM hay ChaCha20-Poly1305, trả giá trên từng byte ở cả hai phía. Với TLS 1.3, Go không cho cấu hình AEAD; nó tự chọn AES-GCM khi CPU có lệnh AES và ChaCha20 khi không, dựa trên cờ CPU mà client gửi gián tiếp qua thứ tự ưu tiên trong ClientHello.
Ba phép đo. A: chín server trên dist-lab, mỗi server một cấu hình, client trên go-lab nối 1.000 lần, đếm byte gửi và nhận trong bắt tay qua một net.Conn bọc ngoài, lấy p50 và p99, và đọc CPU của tiến trình server qua một cổng điều khiển trước và sau để chia ra µs CPU mỗi bắt tay; đồng hồ CPU của Linux có hạt 10 ms nên cần 1.000 lần để số có nghĩa. Trường hợp HelloRetryRequest: client ưu tiên P-256 nên gửi key_share P-256, server chỉ nhận X25519, phải yêu cầu client gửi lại; đo thêm lần nữa dưới netem 50 ms để thấy nó tốn bao nhiêu thời gian thật. B: một tiến trình, loopback, ghi 2 GiB theo khối 16 KB qua tls.Conn, phía kia đọc và bỏ; tính MB/s và ns CPU mỗi byte gồm cả mã hoá lẫn giải mã. C: gọi thẳng Seal của ba AEAD trên khối 16 KB và 1.400 byte, so với openssl speed -evp. Cột "không có AES" không đến từ máy khác: GODEBUG=cpu.aes=off ép crypto/aes bỏ lệnh phần cứng và chạy mã Go thuần, OPENSSL_armcap=0 làm điều tương tự với OpenSSL, nên khác biệt đo được là của thuật toán trên cùng một CPU.
Đo thật: P-521 ×4,6, RSA ×4,3, HRR +1 RTT, không AES ×26

Hình 2: Bắt tay theo từng key exchange và chứng chỉ, chi phí HRR dưới netem, thông lượng theo AEAD có và không có lệnh AES, và primitive Go so với OpenSSL.
| phép đo | kết quả |
|---|---|
| X25519 / P-256 / P-384 / P-521, CPU server mỗi bắt tay | 230 / 200 / 520 / 1.060 µs |
| X25519Kyber768 mặc định | 300 µs, ClientHello 1.556 B thay vì 328 |
| chứng chỉ RSA-2048 thay ECDSA | server 990 µs, nhận 2.453 B thay vì 1.219 |
| TLS 1.2 ECDHE / RSA key exchange | 230 µs / 910 µs, RSA kex bị tắt mặc định |
| HRR, netem 50 ms | 107,5 ms so với 55,5 ms, gửi 606 B thay vì 328 |
| thông lượng AES-128-GCM / ChaCha20, có lệnh AES | 4.523 / 993 MB/s |
| thông lượng AES-128-GCM / ChaCha20, tắt lệnh AES | 168 / 764 MB/s |
| primitive Seal 16 KB, Go: AES-128-GCM / ChaCha20 | 8.918 / 1.221 MB/s, tắt AES: 190 / 1.171 |
X25519 và P-256 ngang nhau, P-384 đắt gấp 2,3 và P-521 gấp 4,6. Server tốn 230 µs CPU cho một bắt tay X25519 và 200 µs cho P-256, trong khoảng nhiễu của nhau; cả hai đều có cài đặt hằng thời gian được tối ưu kỹ trong Go. P-384 lên 520 µs và P-521 lên 1.060 µs, với client cũng tăng tương ứng vì Diffie-Hellman là việc đối xứng. Byte thì gần như không đổi, key share X25519 là 32 byte, P-256 là 65, P-521 là 133. Vậy chọn P-384 hay P-521 "cho an toàn hơn" là trả gấp đôi đến gấp bốn CPU bắt tay, ở cả hai phía, cho một mức an toàn mà phần còn lại của hệ thống, chứng chỉ P-256 và AES-128, không theo kịp. Go đặt thứ tự ưu tiên X25519, P-256, P-384, P-521 là có lý do, và bài này không thấy lý do để đổi.
Kyber lai tốn thêm 70 µs và 1.228 byte ClientHello, không tốn thêm RTT. Cấu hình mặc định của Go 1.23 là X25519Kyber768: server 300 µs thay vì 230, client 390 thay vì 300, gửi 1.556 byte thay vì 328 vì key share Kyber dài 1.216 byte, và nhận 2.308 thay vì 1.219 vì server cũng trả một key share lớn. Khớp với 73 µs mà phần 2 tính ra từ một hướng khác. Dưới netem 50 ms, Kyber bắt tay trong 55,9 ms so với 55,5 ms của X25519: ClientHello 1.556 byte vẫn nằm trong cửa sổ ban đầu của TCP nên không tốn thêm vòng đi về. Hệ quả: chi phí của Kyber là CPU và byte, không phải độ trễ, ít nhất cho tới khi middlebox nào đó không chịu ClientHello lớn hơn một gói, chuyện đã xảy ra với một số thiết bị cũ khi Chrome bật Kyber năm 2024.
Chữ ký RSA-2048 làm server tốn gấp 4,3 lần, và chỉ server. Cùng X25519, đổi chứng chỉ từ ECDSA sang RSA-2048: server 990 µs thay vì 230, client 360 thay vì 300, nhận 2.453 byte thay vì 1.219. Phía client chỉ tốn thêm việc verify một chữ ký RSA, rẻ, và đọc thêm 1.234 byte chứng chỉ. Phía server trả 760 µs cho một chữ ký RSA-2048, khớp với 730 µs phần 2 đo bằng cách khác. Đây là chỗ bất đối xứng quan trọng về mặt vận hành: client không cảm thấy gì, server thì mất bốn phần năm năng lực bắt tay. RSA key exchange của TLS 1.2 cũng rơi vào đúng khoảng đó, 910 µs server, vì server phải giải mã RSA một lần thay vì ký một lần, cùng một phép luỹ thừa khoá riêng. Nó thêm cái tệ là không có forward secrecy, và Go 1.23 từ chối nó mặc định: client gọi server chỉ hỗ trợ TLS_RSA_WITH_AES_128_GCM_SHA256 nhận remote error: tls: handshake failure cho tới khi cả hai phía đặt GODEBUG=tlsrsakex=1. TLS 1.2 với ECDHE thì không đắt hơn TLS 1.3 chút nào, 230 µs như nhau; khác biệt giữa hai phiên bản là ở số vòng đi về và tính năng, không phải ở CPU.
HelloRetryRequest tốn thêm một ClientHello và đúng một RTT. Client ưu tiên P-256 nhưng server chỉ nhận X25519: client gửi 606 byte thay vì 328 vì phải gửi ClientHello hai lần, CPU server 260 thay vì 230 µs. Trên mạng Docker, bắt tay 0,46 thay vì 0,35 ms, không đáng kể. Dưới netem 50 ms chiều server về client, 107,5 ms thay vì 55,5, đúng một RTT cộng thêm. Mạng di động với RTT 100 đến 200 ms thì HRR là một phần tư giây mất không, trên mỗi kết nối mới, chỉ vì hai phía không thống nhất đường cong ưu tiên. Cách tránh là để CurvePreferences mặc định, hoặc nếu phải cố định thì đặt X25519 đầu tiên ở cả client lẫn server; và nhớ ConnectionState của Go không có cờ nào báo HRR đã xảy ra, phải đếm byte hoặc bắt gói mới thấy.
AES-GCM nhanh gấp 4,5 lần ChaCha20 khi có lệnh AES, và chậm gấp 4,5 lần khi không có. Qua tls.Conn trên loopback: plaintext 9.596 MB/s với 0,19 ns CPU mỗi byte cho cả hai đầu, chủ yếu là copy qua socket. AES-128-GCM 4.523 MB/s và 0,42 ns mỗi byte; AES-256-GCM 4.242 và 0,46; ChaCha20-Poly1305 993 MB/s và 1,99 ns. Tức là TLS với AES-GCM trên CPU có lệnh AES chỉ cộng 0,23 ns mỗi byte vào chi phí truyền, một server 10 Gbit/s dành khoảng một phần tư lõi cho mã hoá. Tắt lệnh AES bằng GODEBUG=cpu.aes=off: AES-128-GCM rơi xuống 168 MB/s và 11,85 ns mỗi byte, gấp 26 lần CPU so với khi có lệnh; ChaCha20 gần như không đổi, 764 MB/s và 2,56 ns, vì nó vốn thiết kế cho phép cộng, xoay và XOR trên thanh ghi thường. Primitive nói cùng một chuyện với biên độ lớn hơn: Seal 16 KB AES-128-GCM 8.918 MB/s có lệnh, 190 MB/s không; ChaCha20 1.221 so với 1.171. OpenSSL cho AES-GCM ngang Go, 7.409 MB/s, nhưng ChaCha20 của nó đạt 2.172 MB/s, gấp 1,8 lần Go, nhờ bản NEON mà golang.org/x/crypto chưa có cho arm64.
TLS 1.3 trong Go tự chọn đúng, TLS 1.2 thì không. Cùng tiến trình với cpu.aes=off, kết nối TLS 1.3 không cấu hình gì tự thương lượng TLS_CHACHA20_POLY1305_SHA256 và đạt 972 MB/s, tương đương ChaCha20 có lệnh AES. Còn TLS 1.2 với danh sách cipher suite cố định đặt AES-128-GCM đầu thì vẫn dùng AES và chịu 168 MB/s. Cơ chế của Go: client không có lệnh AES sẽ đẩy ChaCha20 lên đầu danh sách ưu tiên, và server, khi thấy client ưu tiên ChaCha20 và chính nó cũng thiếu lệnh AES, hoặc đơn giản là tôn trọng thứ tự client cho TLS 1.3, sẽ chọn theo. Với TLS 1.2 mà người ta cố định CipherSuites, Go vẫn cố ưu tiên ChaCha20 nếu phát hiện client thiếu AES, nhưng chỉ khi ChaCha20 có trong danh sách; danh sách chỉ có AES thì hết đường. Bài học là một dòng: đừng cố định CipherSuites, và nếu phải cố định cho TLS 1.2 thì luôn để ChaCha20 trong đó.
Đánh đổi cần cân nhắc
Giữ mặc định của Go cho key exchange, trừ khi đo thấy Kyber là vấn đề. X25519 và P-256 là hai lựa chọn rẻ nhất và an toàn đủ; P-384 và P-521 tốn gấp 2 đến 4,6 lần ở cả hai phía mà không mua được gì tương xứng. Kyber mặc định tốn 70 µs và 1,2 KB mỗi bắt tay; chấp nhận được với hầu hết dịch vụ, và nếu không, GODEBUG=tlskyber=0 là công tắc có chủ ý chứ không phải mặc định.
Lo cho client không có lệnh AES, dù server của bạn có. Điện thoại Android rẻ với Cortex-A53 không có phần mở rộng mật mã, và một số VM bị che cờ CPU, sẽ chạy AES-GCM ở tốc độ 168 MB/s với CPU gấp 26 lần, tức là ngốn pin và làm nóng máy để tải một video. ChaCha20 là lựa chọn cho họ, và TLS 1.3 tự xử lý nếu bạn không cố định cipher suite. Ở phía server trung tâm dữ liệu thì AES-GCM, với 0,23 ns mỗi byte, gần như miễn phí.
Phạm vi đo. Một vCPU aarch64 ảo hoá, Go 1.23 với x/crypto v0.31; x86 với AES-NI và VAES sẽ cho AES-GCM còn nhanh hơn và ChaCha20 có bản AVX2 nhanh hơn bản arm64 của Go. Cột "tắt lệnh AES" là giả lập bằng phần mềm trên CPU vẫn có cache và pipeline của máy mạnh, nên số tuyệt đối của Cortex-A53 thật sẽ thấp hơn cả hai thuật toán, dù tỉ lệ vẫn nghiêng về ChaCha20. Chưa đo: AES-CCM, cipher suite TLS 1.2 kiểu CBC và lý do chúng bị loại, và chi phí record 16 KB so với record nhỏ trong HTTP/2.
Ba ý mang về
- Key exchange: X25519 hoặc P-256, 200 đến 230 µs mỗi bên; P-521 gấp 4,6; Kyber mặc định cộng 70 µs và 1,2 KB nhưng không cộng RTT. HRR vì lệch ưu tiên đường cong thì cộng đúng một RTT, 52 ms trên mạng 50 ms.
- Chữ ký là chỗ bất đối xứng: RSA-2048 đẩy server lên 990 µs trong khi client vẫn 360. RSA key exchange của TLS 1.2 cũng đắt tương đương và không có forward secrecy; Go đã tắt nó, đừng bật lại.
- AEAD phụ thuộc CPU, không phụ thuộc thuật toán "tốt hơn": AES-GCM 4.523 MB/s có lệnh AES, 168 MB/s không có; ChaCha20 quanh 800 đến 1.000 ở cả hai. Để TLS 1.3 tự chọn, và nếu cố định cipher suite cho TLS 1.2 thì luôn kèm ChaCha20.
Nguồn
- Go
crypto/tls—Config.CurvePreferences,CipherSuites, thứ tự ưu tiên và logic chọn AES-GCM/ChaCha20: https://pkg.go.dev/crypto/tls#Config - Go 1.22 Release Notes — tắt RSA key exchange mặc định,
GODEBUG=tlsrsakex=1: https://go.dev/doc/go1.22#cryptotlspkgcryptotls - Go 1.23 Release Notes — X25519Kyber768Draft00 mặc định và
tlskyber: https://go.dev/doc/go1.23#cryptotlspkgcryptotls - Go
internal/cpu— biếnGODEBUG=cpu.*để tắt tính năng CPU: https://pkg.go.dev/internal/cpu - RFC 8446 — TLS 1.3, mục 4.1.4 Hello Retry Request và 4.2.8 Key Share: https://www.rfc-editor.org/rfc/rfc8446#section-4.1.4
- RFC 8439 — ChaCha20 and Poly1305 for IETF Protocols: https://www.rfc-editor.org/rfc/rfc8439
- Cloudflare — Do the ChaCha: better mobile performance with cryptography: https://blog.cloudflare.com/do-the-chacha-better-mobile-performance-with-cryptography/
- OpenSSL
openssl-speed(1)và biếnOPENSSL_armcap: https://docs.openssl.org/3.0/man1/openssl-speed/
Phần sau lật ngược hướng xác thực: mTLS, server đòi chứng chỉ của client. Đo chi phí thêm của việc client ký, cách xoay chứng chỉ client và CA trung gian mà không ngắt kết nối nào, và chuyện gì xảy ra với 10.000 kết nối đang mở khi CA cũ hết hạn.