Golang: TLS version and Cipher suites
Photo by Alex Motoc on Unsplash
HTTPS server를 운영할 때에 보안과 관련한 설정에 있어서 TLS version과 Cipher suite에 대해 정리해본다. 지나치게 깊이 들어가지는 않으면서도 전체적인 개념이해가 되는 방향으로 하였다. 큰 개념의 이해후에 각자가 필요한 만큼 좀더 파고들어갈 수 있겠다.
거친 개념정리
- HTTPS로 일단 연결이 되면 중간에 내용을 가로챈다 하여도 안전하다. 이 보안을 담당하는 녀석이 TLS이다.
- 현재 시점(2021-09-27) TLSv1.3을 권장하며, 최소 TLSv1.2 이상을 사용하자. (다만 TLSv1.3은 모니터링에 제약이 있을 수 있다.)
- HTTPS로 보안연결을 하는 작업이 TLS handshake이며 server와 client가 안전하게 대칭키를 공유하는 과정이다.
- 이 과정에 다양한 암호 알고리즘이 사용되는데 그 조합들의 모음이 Cipher suites 이다.
Cipher suites - in Golang source
// 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
Server와 Client의 입장
Server: 나는 최소 TLS, 최대 TLS 지원버전은 이렇고, 지원하는 Cipher suite는 이렇다.
Client: 나는 최소 TLS, 최대 TLS 지원버전은 이렇고, 지원하는 Cipher suite는 이렇다.
연결을 위한 TLS handshake에서는 client와 server가 서로가 맞춰 줄 수 있는 조합을 확인하고 우선순위에 따라 TLS 버전, Cipher suite를 결정하고 이용하게 된다. 즉, "우리 TLS 몇 버전에, cipher suite는 이걸 쓰자" 라고 합의를 하게 되는 것이다.
코드로 보자
GitHub: https://github.com/nicewook/tls-cipher-suite
server와 client의 TLS를 설정하고 각각의 조합에서의 동작을 확인하는 코드를 짜보았다.
전체 코드는 GitHub을 참고하고 각각의 경우를 코드의 해당부분을 통해 확인해보자
TLS server 생성코드이다. TLS의 최소/최대 버전을 설정할 수 있고, 사용하고자 하는 cipher suites를 설정할 수 있다.
// cipher-suite.go
func NewTLSServer(minVersion, maxVersion uint16, cipherSuites []uint16) *http.Server {
mux := http.NewServeMux()
mux.HandleFunc("/", simpleHandler)
cfg := &tls.Config{}
if minVersion != 0 {
cfg.MinVersion = minVersion
}
if maxVersion != 0 {
cfg.MaxVersion = maxVersion
}
if len(cipherSuites) != 0 {
cfg.CipherSuites = make([]uint16, len(cipherSuites))
copy(cfg.CipherSuites, cipherSuites)
}
return &http.Server{
Addr: ":8443",
ConnState: connStateHook,
Handler: mux,
TLSConfig: cfg,
}
}
Client 생성코드이다. 원하는 tls.Config에 따라 생성이 가능하다.
// cipher-suite.go
func NewTLSClient(tlsConfig *tls.Config) *http.Client {
return &http.Client{Transport: &http.Transport{
TLSClientConfig: tlsConfig,
}}
}
func tlsConfigDefault() *tls.Config {
return &tls.Config{
InsecureSkipVerify: true,
}
}
func tlsConfigV10() *tls.Config {
return &tls.Config{
InsecureSkipVerify: true,
MaxVersion: tls.VersionTLS10,
}
}
func tlsConfigV12() *tls.Config {
return &tls.Config{
InsecureSkipVerify: true,
MaxVersion: tls.VersionTLS12,
}
}
기본 테스트
// cipher-suite_test.go
// test default
t.Run("Default TLS", func(t *testing.T) {
var (
minVer uint16
maxVer uint16
cipherSuites []uint16
)
server := NewTLSServer(minVer, maxVer, cipherSuites)
defer func() {
if err := server.Shutdown(context.TODO()); err != nil {
t.Error(err) // failure/timeout shutting down the server gracefully
}
}()
go func() {
if err := server.ListenAndServeTLS("server.crt", "server.key"); err != nil &&
err != http.ErrServerClosed {
t.Error(err)
}
}()
time.Sleep(1 * time.Second)
client := NewTLSClient(tlsConfigDefault())
doRequest(client)
})
server와 client 모두 TLS와 관련한 어떤 설정도 하지 않고 테스트하였다.
결과는 아래와 같이 TLS1.3으로 협의가 되고, TLS1.3의 CipherSuite중 하나를 선택한 것을 알 수 있다.
START| TLSCipherSuites/Default_TLS
| 2021/09/27 15:58:12 Negociated TLS version: VersionTLS13
| 2021/09/27 15:58:12 Negociated CipherSuite: TLS_AES_128_GCM_SHA256
| === CONT TestTLSCipherSuites
| cipher-suite_test.go:27: Code: 200
| cipher-suite_test.go:28: Body: This is an example server
서버가 TLSv1.2 까지만 지원하는 경우
// cipher-suite_test.go
// test maxVersion TLS
t.Run("maxVersion TLSv1.2", func(t *testing.T) {
var (
minVer uint16
maxVer uint16
cipherSuites []uint16
)
maxVer = uint16(tls.VersionTLS12)
server := NewTLSServer(minVer, maxVer, cipherSuites)
defer func() {
if err := server.Shutdown(context.TODO()); err != nil {
t.Error(err) // failure/timeout shutting down the server gracefully
}
}()
go func() {
if err := server.ListenAndServeTLS("server.crt", "server.key"); err != nil &&
err != http.ErrServerClosed {
t.Error(err)
}
}()
time.Sleep(1 * time.Second)
client := NewTLSClient(tlsConfigDefault())
doRequest(client)
})
협상이(Negiciation) TLS1.2로 되었다.
CipherSuite는 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 이다. TLS1.3이 지원하는 CipherSuite가 아니다.
START| TLSCipherSuites/maxVersion_TLSv1.2
| 2021/09/27 15:58:13 Negociated TLS version: VersionTLS12
| 2021/09/27 15:58:13 Negociated CipherSuite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
| === CONT TestTLSCipherSuites
| cipher-suite_test.go:27: Code: 200
| cipher-suite_test.go:28: Body: This is an example server
서버는 TLS1.0에서 TLS1.3까지 지원하지만 Client가 TLS1.0까지만 지원하는 경우
// cipher-suite_test.go
// test client maxVersion TLSv1.0
t.Run("client maxVersion TLSv1.0", func(t *testing.T) {
var (
minVer uint16
maxVer uint16
cipherSuites []uint16
)
server := NewTLSServer(minVer, maxVer, cipherSuites)
defer func() {
if err := server.Shutdown(context.TODO()); err != nil {
t.Error(err) // failure/timeout shutting down the server gracefully
}
}()
go func() {
if err := server.ListenAndServeTLS("server.crt", "server.key"); err != nil &&
err != http.ErrServerClosed {
t.Error(err)
}
}()
time.Sleep(1 * time.Second)
client := NewTLSClient(tlsConfigV10())
doRequest(client)
})
서버는 default 설정이지만 Client를 보면 tlsConfigV10() 함수를 이용하여 MaxVersion을 tls.VersionTLS10으로 제한했다.
예상대로 아래와 같이 TLS1.0으로 협상이 되었다.
START| TLSCipherSuites/client_maxVersion_TLSv1.0
| 2021/09/27 15:58:14 Negociated TLS version: VersionTLS10
| 2021/09/27 15:58:14 Negociated CipherSuite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
| === CONT TestTLSCipherSuites
| cipher-suite_test.go:27: Code: 200
| cipher-suite_test.go:28: Body: This is an example server
서버가 특정 CipherSuite만 지원하는 경우
// cipher-suite_test.go
// test specific CipherSuite
t.Run("server specific CipherSuite", func(t *testing.T) {
var (
minVer uint16
maxVer uint16
cipherSuites []uint16
)
minVer = tls.VersionTLS12
maxVer = tls.VersionTLS12
cipherSuites = []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}
server := NewTLSServer(minVer, maxVer, cipherSuites)
defer func() {
if err := server.Shutdown(context.TODO()); err != nil {
t.Error(err) // failure/timeout shutting down the server gracefully
}
}()
go func() {
if err := server.ListenAndServeTLS("server.crt", "server.key"); err != nil &&
err != http.ErrServerClosed {
t.Error(err)
}
}()
time.Sleep(1 * time.Second)
t.Log("expect: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256")
client := NewTLSClient(tlsConfigDefault())
doRequest(client)
})
이러한 경우는 잘 없지만, 그리고 이 경우 좀더 들여다 봐야 할 변수들도 있지만 참고삼아 테스트 하였다.
(좀 더 들여다볼 키워드 HTTP/2-required AES_128_GCM_SHA256 cipher)
허용한 CipherSuite인 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256로 협상이 되었다.
START| TLSCipherSuites/server_specific_CipherSuite
| cipher-suite_test.go:131: expect: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
| 2021/09/27 15:58:15 Negociated TLS version: VersionTLS12
| 2021/09/27 15:58:15 Negociated CipherSuite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
| === CONT TestTLSCipherSuites
| cipher-suite_test.go:27: Code: 200
| cipher-suite_test.go:28: Body: This is an example server
마지막으로, 협상에 실패하는 경우
// cipher-suite_test.go
// test client max version is lower then server
t.Run("client max version is lower then server", func(t *testing.T) {
var (
minVer uint16
maxVer uint16
cipherSuites []uint16
)
minVer = tls.VersionTLS13
server := NewTLSServer(minVer, maxVer, cipherSuites)
defer func() {
if err := server.Shutdown(context.TODO()); err != nil {
t.Error(err) // failure/timeout shutting down the server gracefully
}
}()
go func() {
if err := server.ListenAndServeTLS("server.crt", "server.key"); err != nil &&
err != http.ErrServerClosed {
t.Error(err)
}
}()
time.Sleep(1 * time.Second)
client := NewTLSClient(tlsConfigV12())
doRequest(client)
})
server는 TLS1.3 이상만을 허용하는데 client는 최대 TLS1.2까지만 지원하니 협상이 실패하였다.
START| TLSCipherSuites/client_max_version_is_lower_then_server
| 2021/09/27 15:58:16 http: TLS handshake error from 127.0.0.1:35254: tls: client offered only unsupported versions: [303 302 301]
| === CONT TestTLSCipherSuites
| cipher-suite_test.go:17: Get "https://127.0.0.1:8443": remote error: tls: protocol version not supported