검색하기귀찮아서만든블로그

[GO] TLS1.3 통신 (TCP) 본문

카테고리 없음

[GO] TLS1.3 통신 (TCP)

hellworld 2023. 9. 28. 11:24

지난 포스팅 (https://access-violation.tistory.com/38) 에서 go 언어를 사용해서 간단한 proxy 서버를 구성해 보았다. 이 번에는 go 언어가 암/복호화에 개발에 용이하다고 하니 TLS1.3 TCP 통신을 구현해 보고자 한다. TLS 통신을 위해서 LS 서버에서 인증서와 개인키 파일을 사용해야 하기 때문에  인증서 생성기가 필요한 것 같고, TLS 통신하는 서버와 클라이언트가 필요할 것으로 생각된다. 

사설 인증서를 임의로 생성하기 위해 다음과 같은 코드를 작성하였다. TLS 및 인증서에 대해서 몇 번 다뤄본 경험이 있어서 다소 쉽게 작성할 수 있었다. 

// 인증서 생성 소스
package main

import (
	"crypto/rand"      // 암호화에 안전한 난수 생성기
	"crypto/rsa"       // RSA 알고리즘
	"crypto/x509"      // 인증서에 대한 X.509 표준
	"crypto/x509/pkix" // 인증서의 PKIX 프로파일
	"encoding/pem"     // 이진 데이터를 위한 PEM 인코딩
	"fmt"
	"math/big"
	"os"
	"time"
)

func main() {
	certFile := "server.crt" // 인증서 파일명
	keyFile := "server.key"  // 개인키 파일명

	if _, err := os.Stat(certFile); !os.IsNotExist(err) {
		fmt.Println("인증서 파일이 이미 존재합니다")
		return
	}

	// 2048비트 개인 키를 생성
	priv, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		fmt.Printf("개인 키 생성 실패: %v\n", err)
		return
	}

	// 인증서 유효 기간 180일
	notBefore := time.Now()
	notAfter := notBefore.Add(180 * 24 * time.Hour)
	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
	// 시리얼 번호 랜덤 생성
	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
	// x509 구조체 생성
	template := x509.Certificate{
		SerialNumber: serialNumber,
		Subject: pkix.Name{
			Organization: []string{"hell world"},
		},
		NotBefore: notBefore,
		NotAfter:  notAfter,

		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		BasicConstraintsValid: true,
		DNSNames:              []string{"hellworld.com"},
	}

	// 개인키를 사용해서 DER를 생성.
	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
	if err != nil {
		fmt.Printf("인증서 생성 실패: %v\n", err)
		return
	}

	// crt 파일을 생성
	certOut, err := os.Create(certFile)
	if err != nil {
		fmt.Printf("%s 파일 열기 실패: %s\n", certFile, err.Error())
		return
	}

	// pem 블록을 certOut에 기록
	pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
	certOut.Close()

	// 키 파일을 오픈한다. (쓰기전용, 이미 있으면 잘릴 수 있음)
	keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		fmt.Printf("%s 파일 열기 실패: %s\n", keyFile, err.Error())
		return
	}

	// pem 블록을 키 파일에 기록
	pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(priv)})
	keyOut.Close()
}

다음은 TLS 서버 코드이다. 앞서 gen_cert.exe 툴을 사용하여 생성한 인증서를 로드해서 TLS 통신 소켓을 생성하고 클라이언트가 접속하면 데이터를 수/송신하고 종료하는 코드이다.

// TLS server 소스
package main

import (
	"crypto/tls"
	"fmt"
)

func main() {
	// TLS 인증서 및 개인키를 로드한다.
	Cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
	if err != nil {
		fmt.Printf("error: %v\n", err)
		return
	}

	// 인증서 설정을 로드한다.
	CertInfo := &tls.Config{
		Certificates: []tls.Certificate{Cert},
		MinVersion:   tls.VersionTLS12,
	}

	fmt.Println("\n서버 시작, 클라이언트의 접속을 대기중... (55000)")
	sockServer, err := tls.Listen("tcp", ":55000", CertInfo)
	if err != nil {
		fmt.Printf("error: %v\n", err)
		return
	}

	// 함수 종료 시 소멸 처리
	defer sockServer.Close()

	// 클라이언트 접속을 대기한다.
	sockClient, err := sockServer.Accept()
	if err != nil {
		fmt.Printf("error: %v\n", err)
		return
	}

	fmt.Println(sockClient.RemoteAddr().String(), "클라이언트가 접속하였습니다.")

	// 데이터 수신을 대기한다.
	buf := make([]byte, 1024)
	nRetVal, err := sockClient.Read(buf)
	if err != nil {
		fmt.Printf("error: %v\n", err)
		return
	}

	fmt.Printf("수신데이터: %s\n", string(buf[:nRetVal]))

	// 클라이언트로 데이터를 송신한다.
	strMessage := []byte("hi client :)")
	sockClient.Write(strMessage)

	// 세션을 종료한다.
	fmt.Println(sockClient.RemoteAddr().String(), "클라이언트와의 연결을 종료합니다.")
	sockClient.Close()
}

다음은 TLS 클라이언트 소스이다. TLS 서버로 접속해서 데이터를 송/수신하고 종료하는 간단한 코드이다.

// TLS client 소스
package main

import (
	"crypto/tls"
	"fmt"
	"log"
)

func main() {
	// TLS 설정을 로드한다.
	conf := &tls.Config{
		InsecureSkipVerify: true,
		MinVersion:         tls.VersionTLS12,
	}

	fmt.Println("\n서버로 접속 시도... (55000)")
	sockClient, err := tls.Dial("tcp", "DESKTOP-H28HDNI:55000", conf)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("서버로 데이터 송신")
	byMessage := []byte("hi server :)")
	sockClient.Write(byMessage)

	byBuff := make([]byte, 1024)
	nReadLen, err := sockClient.Read(byBuff)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("데이터수신: %s\n", string(byBuff[:nReadLen]))

	sockClient.Close()
}

코드는 각각의 폴더에 main.go 파일로 생성했고 go build -o <출력파일명> <소스경로> 형식으로 빌드 한다.

프로젝트를 빌드

빌드가 완료되면 gen_cert.exe, tls_cli.exe, tls_svr.exe 파일 3개가 생성되고, getn_cert.exe를 실행하면 server.crt 인증서와 server.key 개인키가 생성된다. 이제 TLS 서버와 TLS 클라이언트를 실행해 보자.

VM-A - tls_svr.exe 실행 화면
tls_cli.exe 실행 화면

서버와 클라이언트 사이에 통신을 정상적으로 되는 것을 볼 수 있다. 이제 TLS 통신이 되었는지 확인하기 위해서 wireshark로 확인해 보자.

wireshark tls 통신 확인

wireshark에서 패킷 분석 내용을 보면 TLS1.3으로 통신하는 것을 확인할 수 있다. Application Data 부분이 서버와 클라이언트가 주고받은 데이터 내용인데 TLS 암호화 통신을 사용하기 때문에 실제 raw data는 확인할 수 없다.

 

이상 go 언어를 사용하여 TLS 1.3 통신하는 기능을 구현해 보았다. 실제로 서비스를 구현할 때 모든 통신 구간은 암호화가 기본이고 통상적으로 TLS 통신을 사용하기 때문에 다음에 참고하기 좋은 샘플 코드라 생각이 된다.

go 언어는 비교적 다른 언어보다 진입 장벽이 낮고 파이썬처럼 간결한 코드 작성이 가능해서 생산성 향상에 도움이 되는 것 같다.