Permalink to this article – https://ift.tt/VhNcyn3
Except for some review articles and translations, the topic selection of my articles mostly comes from the problems encountered in actual work and study. This time, let’s talk about a problem we encountered recently: how to speed up the establishment of massive connections based on TLS secure communication ?
Below the TLS (Transport Layer Security) transmission security layer is the TCP layer. The first thing we may think of is to optimize the relevant parameters of the kernel’s TCP handshake to quickly establish a TCP connection, such as:
- net.ipv4.tcp_max_syn_backlog
- net.ipv4.tcp_syncookies
- net.ipv4.tcp_synack_retries
- net.ipv4.tcp_abort_on_overflow
- net.core.somaxconn
- …
Regarding the tuning of Linux kernel parameters, you can refer to the Geek Time column “System Performance Tuning Must Know”
In addition, in order to speed up the establishment of massive connections and increase the speed of applications obtaining connections from the kernel accept connection queue, we can also use the mechanism of multi-thread/multi-goroutine concurrently listening to the same port and accepting concurrently. If you use the Go language, You can take a look at the go-reuseport package.
After talking about the TCP layer, is there anything that can be optimized in the TLS layer that affects the speed of connection establishment? Yes, that is to use TLS 1.3 version to speed up the handshake process, thereby speeding up the speed of connection establishment. TLS 1.3 is a new TLS standard released in 2018, and it has only been supported by mainstream languages, browsers, and web servers in the past 2-3 years. So how does it differ from the previous TLS 1.2? How well does Go support TLS version 1.3? How to write secure communication code using TLS 1.3 in Go? How much faster is the connection establishment speed of TLS 1.3 than TLS 1.2?
With these questions in mind, we enter the main body of this article! Let’s first briefly look at the differences between TLS 1.3 and TLS 1.2 versions.
1. Differences between TLS 1.3 and TLS 1.2
TLS is a secure connection protocol standard based on encryption, decryption and signature algorithms developed and released by the Internet Engineering Task Force ( IETF ) to replace SSL. Its evolution process is as follows:
Among them, TLS 1.0 and 1.1 versions will be invalidated in 2020 because they are no longer safe. The current mainstream version and the most widely used version is TLS 1.2 version released in 2008 (the proportion of usage is as shown in the figure below), and the latest version is 2018 . TLS 1.3 was officially released in 2010 , and the release of TLS 1.3 also means that TLS 1.2 has entered the “obsolete period”, although it will take a long time for TLS 1.2 to “go offline” in practice:
TLS 1.3 is not compatible with TLS 1.2. In the TLS 1.3 protocol specification, we can see some major changes of TLS 1.3 compared to TLS 1.2 listed:
- Removed non-AEAD (Authenticated Encryption with Associated Data) algorithms in the original symmetric encryption algorithm list, including 3DES, RC4, AES-CBC, etc., and only supports more secure encryption algorithms.
Note: Common AEAD algorithms include: AES-128-GCM, AES-256-GCM, ChaCha20-IETF-Poly1305, etc. On the CPU (desktop, server) with AES acceleration, it is recommended to use the AES-XXX-GCM series, and the mobile device is recommended to use the ChaCha20-IETF-Poly1305 series.
- Static RSA and Diffie-Hellman cipher suites have been removed; all public key-based key exchange mechanisms now provide forward secrecy.
Note: Forward Secrecy means that the leakage of the long-term used master key will not lead to the leakage of past session keys. Forward secrecy protects past communications from future exposure of passwords or keys. If the system is forward secure, it can guarantee the security of historical communication when the master key is compromised, even if the system is actively attacked.
- The negotiation mechanism of the TLS 1.2 version has been abandoned, and a faster new key negotiation mechanism has been introduced: PSK (Pre-Shared Key), which simplifies the handshake process (the figure below is a comparison of the TLS 1.2 and TLS 1.3 handshake processes). At the same time, all handshake information after ServerHello is now encrypted, and various extended information previously sent in clear text in ServerHello can now also enjoy encryption protection.
- Added a zero-round-trip-time (0-RTT) mode, which saves a round-trip time for some application data when resuming connection establishment. But at the cost of losing some security properties.
Note: When a client (such as a browser) successfully completes the TLS 1.3 handshake with a server for the first time, both the client and the server can store a pre-shared encryption key locally, which is called a recovery master key. If the client establishes a connection with the server again later, it can use this recovery key to send encrypted application data in its first message to the server without performing a second handshake. 0-RTT mode has a security weakness. Sending data through recovery mode does not require any interaction from the server, which means that an attacker (usually a middle-man) can capture encrypted 0-RTT data and then resend it to the server, or replay it (Replay) they. The solution to this problem is to ensure that all 0-RTT requests are idempotent.
Among these major changes, the one related to the initial connection speed is obviously the change of the TLS 1.3 handshake mechanism: shortening from 2-RTT to 1-RTT (as shown in the figure above). Let’s use Go as an example to see how TLS 1.3 improves connection speed compared to TLS 1.2.
2. Go supports TLS 1.3
-
The Go language provides optional support for TLS 1.3 starting from Go 1.12 . In Go 1.12 version, TLS 1.3 will be enabled if you set GODEBUG=tls13=1 and do not explicitly set the MaxVersion of tls Config. The implementation of this version does not support the 0-RTT feature of TLS 1.3.
-
Go version 1.13 enables TLS 1.3 by default, you can use GODEBUG=tls13=0 to disable support for TLS 1.3.
-
After Go 1.14 version , TLS 1.3 becomes the default TLS version option and can no longer be turned off with GODEBUG=tls13=0! However, the TLS version to use can be configured via Config.MaxVersion.
-
In Go 1.16 version , if the server or client does not support AES hardware acceleration, the server will give priority to other AEAD cipher suites (cipher suites), such as ChaCha20Poly1305, instead of AES-GCM cipher suites.
-
In Go 1.18 version , the Config.MinVersion on the client side will default to TLS 1.2, replacing the original default value of TLS 1.0/TLS 1.1. You can however change this by explicitly setting the client’s Config.MinVersion. However, this change does not affect the server side.
With that in mind, let’s look at a simple client and server example using Go and TLS version 1.3.
3. Go TLS 1.3 client-server communication example
This time we will not refer to the sample of the crypto/tls package in the Go standard library, but let’s do something fashionable: use AI to assist in generating a set of TLS-based client-server communication code examples. ChatGPT is not open to the mainland. I use the AI programming assistant (AICodeHelper) here. The following is a screenshot of the generation process:
AICodeHelper generated most of the code for us, but the server-side code has two problems: it can only handle one client-side connection and does not generate the incoming server certificate and private key code segment. We make some modifications based on the above framework code and get Our server and client code:
server端代码: // https://github.com/bigwhite/experiments/blob/master/go-and-tls13/server.go package main import ( "bufio" "crypto/tls" "fmt" "net" ) func main() { cer, err := tls.LoadX509KeyPair("server.crt", "server.key") if err != nil { fmt.Println(err) return } config := &tls.Config{Certificates: []tls.Certificate{cer}} ln, err := tls.Listen("tcp", "localhost:8443", config) if err != nil { fmt.Println(err) return } defer ln.Close() for { conn, err := ln.Accept() if err != nil { fmt.Println(err) continue } go handleConnection(conn) } } func handleConnection(conn net.Conn) { defer conn.Close() r := bufio.NewReader(conn) for { msg, err := r.ReadString('\n') if err != nil { fmt.Println(err) return } println(msg) n, err := conn.Write([]byte("hello, world from server\n")) if err != nil { fmt.Println(n, err) return } } }
// https://github.com/bigwhite/experiments/blob/master/go-and-tls13/client.go package main import ( "crypto/tls" "log" ) func main() { conf := &tls.Config{ InsecureSkipVerify: true, } conn, err := tls.Dial("tcp", "localhost:8443", conf) if err != nil { log.Println(err) return } defer conn.Close() n, err := conn.Write([]byte("hello, world from client\n")) if err != nil { log.Println(n, err) return } buf := make([]byte, 100) n, err = conn.Read(buf) if err != nil { log.Println(n, err) return } println(string(buf[:n])) }
For convenience, a self-signed certificate is used here, and the client does not verify the public key digital certificate of the server (we do not need to generate the relevant key and certificate for creating a CA), we only need to use the following command to generate a pair of server.key and server .crt:
$openssl genrsa -out server.key 2048 Generating RSA private key, 2048 bit long modulus ..........................+++ ................................+++ e is 65537 (0x10001) $openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) []: State or Province Name (full name) []: Locality Name (eg, city) []: Organization Name (eg, company) []: Organizational Unit Name (eg, section) []: Common Name (eg, fully qualified host name) []:localhost Email Address []:
For details about asymmetric encryption and digital certificates, please refer to Article 51 “Using the net/http package to achieve secure communication” in my book “The Road to Go Language Improvement” .
Run Server and Client, here I am using the Go 1.19 version compiler:
$go run server.go hello, world from client EOF $go run client.go hello, world from server
Our example is already working! So how to prove that the TLS connection of version 1.3 is used between the client and the server in the example? Or how to check which TLS version is used between client and server?
Some friends may say: Use wireshark to capture packets, this is feasible, but use wireshark to capture tls packets, especially the 1.3 linking packets are more laborious. We have a simpler way, we can achieve it by modifying the standard library in the development environment. Let’s continue reading.
4. Selection of TLS version on server and client
The TLS handshake process is initiated by the client. From the perspective of the client, when the client receives the serverHello response, it can obtain the TLS version to be used after the decision. So here we modify the clientHandshake method of crypto/tls/handshake_client.go, and use fmt.Printf to output the information related to the TLS connection in its implementation (see the output at the beginning of “====” in the code below):
// $GOROOT/src/crypto/tls/handshake_client.go func (c *Conn) clientHandshake(ctx context.Context) (err error) { ... ... hello, ecdheParams, err := c.makeClientHello() if err != nil { return err } c.serverName = hello.serverName fmt.Printf("====client: supportedVersions: %x, cipherSuites: %x\n", hello.supportedVersions, hello.cipherSuites) ... ... msg, err := c.readHandshake() if err != nil { return err } serverHello, ok := msg.(*serverHelloMsg) if !ok { c.sendAlert(alertUnexpectedMessage) return unexpectedMessageError(serverHello, msg) } if err := c.pickTLSVersion(serverHello); err != nil { return err } ... ... if c.vers == VersionTLS13 { fmt.Printf("====client: choose tls 1.3, server use ciphersuite: [0x%x]\n", serverHello.cipherSuite) ... ... // In TLS 1.3, session tickets are delivered after the handshake. return hs.handshake() } fmt.Printf("====client: choose tls 1.2, server use ciphersuite: [0x%x]\n", serverHello.cipherSuite) hs := &clientHandshakeState{ ... ... } if err := hs.handshake(); err != nil { return err } ... ... }
After modifying the standard library, let’s run the above client.go again:
$go run client.go ====client: supportedVersions: [304 303], cipherSuites: [c02b c02f c02c c030 cca9 cca8 c009 c013 c00a c014 9c 9d 2f 35 c012 a 1301 1302 1303] ====client: choose tls 1.3, server use ciphersuite: [0x1301] hello, world from server
Here we look at the content of the first line of output. Here, the output is the content in the clientHello handshake package built by the client. It shows the TLS version and cipher suites supported by the client. We see that the client supports 0× 304, 0×303 two TLS versions, these two numbers correspond to the constants in the following code respectively:
// $GOROOT/src/crypto/tls/common.go const ( VersionTLS10 = 0x0301 VersionTLS11 = 0x0302 VersionTLS12 = 0x0303 VersionTLS13 = 0x0304 // Deprecated: SSLv3 is cryptographically broken, and is no longer // supported by this package. See golang.org/issue/32716. VersionSSL30 = 0x0300 )
The hexadecimal numbers contained in the output cipherSuites come from the following constants:
// $GOROOT/src/crypto/tls/cipher_suites.go const ( // TLS 1.0 - 1.2 cipher suites. TLS_RSA_WITH_RC4_128_SHA uint16 = 0x0005 TLS_RSA_WITH_3DES_EDE_CBC_SHA uint16 = 0x000a TLS_RSA_WITH_AES_128_CBC_SHA uint16 = 0x002f TLS_RSA_WITH_AES_256_CBC_SHA uint16 = 0x0035 TLS_RSA_WITH_AES_128_CBC_SHA256 uint16 = 0x003c TLS_RSA_WITH_AES_128_GCM_SHA256 uint16 = 0x009c TLS_RSA_WITH_AES_256_GCM_SHA384 uint16 = 0x009d TLS_ECDHE_ECDSA_WITH_RC4_128_SHA uint16 = 0xc007 TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA uint16 = 0xc009 TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA uint16 = 0xc00a TLS_ECDHE_RSA_WITH_RC4_128_SHA uint16 = 0xc011 TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA uint16 = 0xc012 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA uint16 = 0xc013 TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA uint16 = 0xc014 TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 uint16 = 0xc023 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 uint16 = 0xc027 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 uint16 = 0xc02f TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 uint16 = 0xc02b TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 uint16 = 0xc030 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 uint16 = 0xc02c TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 uint16 = 0xcca8 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 uint16 = 0xcca9 // TLS 1.3 cipher suites. TLS_AES_128_GCM_SHA256 uint16 = 0x1301 TLS_AES_256_GCM_SHA384 uint16 = 0x1302 TLS_CHACHA20_POLY1305_SHA256 uint16 = 0x1303 ... ... }
From the second line of output in the client.go running results, we can see that the two parties finally chose TLS 1.3 version and the cipher suite TLS_AES_128_GCM_SHA256 for this connection establishment. This is consistent with the previous description in our review of the Go language’s support history for TLS 1.3. TLS 1.3 is the default choice for the built-in version.
So can we choose the version used when establishing a connection? Of course, we can configure both on the server side and on the client side. Let’s take a look at how to configure it on the Server side:
// https://github.com/bigwhite/experiments/blob/master/go-and-tls13/server_tls12.go func main() { cer, err := tls.LoadX509KeyPair("server.crt", "server.key") if err != nil { fmt.Println(err) return } config := &tls.Config{ Certificates: []tls.Certificate{cer}, MaxVersion: tls.VersionTLS12, } ... ... }
We created server_tls12.go based on server.go. In this new source file, we add a configuration MaxVersion to tls.Config and set its value to tls.VersionTLS12, which means that the highest supported TLS version is TLS 1.2 . In this way, when we use client.go to establish a connection with the server program running based on server_tls12.go, we will get the following output:
$go run client.go ====client: supportedVersions: [304 303], cipherSuites: [c02b c02f c02c c030 cca9 cca8 c009 c013 c00a c014 9c 9d 2f 35 c012 a 1301 1302 1303] ====client: choose tls 1.2, server use ciphersuite: [0xc02f] hello, world from server
We can see that the two sides of the interaction finally chose the TLS 1.2 version, and the cipher suite used is 0xc02f, that is, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256.
Similarly, if you want to configure the client to support up to TLS 1.2 version, you can also use the same method. You can take a look at the source file client_tls12.go in the corresponding code base of this article, so I won’t go into details here.
At this point, some friends may have a question: we can configure the version of TLS used, so for TLS 1.3, can we configure the cipher suite to be used? The answer is currently not possible, the reason comes from the comment of the Config.CipherSuites field: “Note that TLS 1.3 ciphersuites are not configurable”:
// $GOROOT/src/crypto/tls/common.go type Config struct { ... ... // CipherSuites is a list of enabled TLS 1.0–1.2 cipher suites. The order of // the list is ignored. Note that TLS 1.3 ciphersuites are not configurable. // // If CipherSuites is nil, a safe default list is used. The default cipher // suites might change over time. CipherSuites []uint16 ... ... }
The tls package will select the cipher suite according to whether the system supports AES acceleration. If AES acceleration is supported, the following defaultCipherSuitesTLS13 will be used, so that AES-related suites will be selected first, otherwise defaultCipherSuitesTLS13NoAES will be used, and TLS_CHACHA20_POLY1305_SHA256 will be preferred:
// $GOROOT/src/crypto/tls/cipher_suites.go // defaultCipherSuitesTLS13 is also the preference order, since there are no // disabled by default TLS 1.3 cipher suites. The same AES vs ChaCha20 logic as // cipherSuitesPreferenceOrder applies. var defaultCipherSuitesTLS13 = []uint16{ TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, } var defaultCipherSuitesTLS13NoAES = []uint16{ TLS_CHACHA20_POLY1305_SHA256, TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, }
Note: joe shaw once wrote an article “Abusing go:linkname to customize TLS 1.3 cipher suites” , which describes a method of customizing TLS 1.3 cipher suites through go:linkname. Interested friends can read it .
5. Connection establishment speed benchmark
Finally, let’s take a look at how much faster the connection establishment speed of TLS 1.3 is compared to TLS 1.2. Considering the difference in the number of RTTs between the two versions, that is, the network delay has a greater impact on the speed of connection establishment, I deliberately chose a network with a ping of 20-30ms. We build Benchmark Tests for TLS 1.2 and TLS 1.3 respectively:
// https://github.com/bigwhite/experiments/blob/master/go-and-tls13/benchmark/benchmark_test.go package main import ( "crypto/tls" "testing" ) func tls12_dial() error { conf := &tls.Config{ InsecureSkipVerify: true, MaxVersion: tls.VersionTLS12, } conn, err := tls.Dial("tcp", "192.168.11.10:8443", conf) if err != nil { return err } conn.Close() return nil } func tls13_dial() error { conf := &tls.Config{ InsecureSkipVerify: true, } conn, err := tls.Dial("tcp", "192.168.11.10:8443", conf) if err != nil { return err } conn.Close() return nil } func BenchmarkTls13(b *testing.B) { b.ReportAllocs() for i := 0; i < bN; i++ { err := tls13_dial() if err != nil { panic(err) } } } func BenchmarkTls12(b *testing.B) { b.ReportAllocs() for i := 0; i < bN; i++ { err := tls12_dial() if err != nil { panic(err) } } }
The server is deployed on 192.168.11.10. For each benchmark test, we give 10s of test time. The following are the running results:
$go test -benchtime 10s -bench . goos: linux goarch: amd64 pkg: demo cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz BenchmarkTls13-8 216 56036809 ns/op 47966 B/op 608 allocs/op BenchmarkTls12-8 145 82395933 ns/op 26655 B/op 283 allocs/op PASS ok demo 37.959s
We see that compared with TLS 1.2, the connection establishment speed of TLS 1.3 is indeed faster. However, from the perspective of memory allocation, the implementation of Go TLS 1.3 seems to be more complicated.
6. References
- RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3 – https://ift.tt/d82OsvE
- Forward Security – https://ift.tt/SjV2EbT
The source code involved in this article can be downloaded here .
“Gopher Tribe” knowledge planet aims to create a boutique Go learning and advanced community! High-quality first release of Go technical articles, “three days” first reading right, analysis of the development status of Go language twice a year, fresh Gopher daily 1 hour in advance every day, online courses, technical columns, book content preview, must answer within six hours Guaranteed to meet all your needs about the Go language ecology! In 2022, the Gopher tribe will be fully revised, and will continue to share knowledge, skills and practices in the Go language and Go application fields, and add many interactive forms. Everyone is welcome to join!
The well-known cloud hosting service provider DigitalOcean released the latest hosting plan. The entry-level Droplet configuration is upgraded to: 1 core CPU, 1G memory, 25G high-speed SSD, and the price is 5$/month. Friends who need to use DigitalOcean, you can open this link address : https://ift.tt/AJmM1GZ to start your DO host road.
Gopher Daily archive repository – https://ift.tt/e3YaPEK
my contact information:
- Weibo (temporarily unavailable): https://ift.tt/KacPqQx
- Weibo 2: https://ift.tt/3TOJPla
- Blog: tonybai.com
- github: https://ift.tt/e8RqTnL
Business cooperation methods: writing, publishing, training, online courses, partnerships, consulting, advertising cooperation.
© 2023, bigwhite . All rights reserved.
This article is transferred from https://tonybai.com/2023/01/13/go-and-tls13/
This site is only for collection, and the copyright belongs to the original author.