Search
Duplicate
📒

[Network Study] 03-2. 전송계층 보안(SSL/TLS)

상태
완료
수업
Network Study
주제
HTTP
연관 노트
3 more properties
참고

SSL/TLS 통신

NOTE
SSL/TLS은 보안 통신을 제공하는 프로토콜로, 기밀성과 데이터 무결성을 보장하여 민감한 정보의 안전한 전송을 도와줍니다.
핸드 셰이크 → 키 교환 → 세션 종료
TLS의 초기 버전은 SSL이라고 불렸지만, 현재 SSL의 취약점이 알려져서 RFC에서는 권장하지 않습니다. SSL 3.0 → TLS 1.0 이후로 대부분 TLS가 사용되는 것이 일반적입니다.
TLS는 응용계층과 전송계층의 사이에 위치한다!
HTTP/1.1에서 부터 본격적으로 사용되어 왔지만, TLS암호화 자체는 HTTP뿐만 아니라 다른 프로토콜에 통신 경로의 안전성을 추가해 새로운 프로토콜로 만들어 낼 수 있는 범용적인 구조로 만들어졌다.
ex) HTTP(80) → HTTPS(443), SMTP(25) → SMTPS(465)
TLS를 통해 안정된 통신로가 생기므로, 웹소켓과 같은 통신 프로토콜이나 HTTP/2와 같은 더 효율적인 프로토콜을 구축하기위한 기반을 마련했습니다.
TLS의 경우 실제 통신 내용은 암호화된 이진 데이터이므로 내부를 보는 것은 어렵지만, curl 명령어로 접근할 때는 URL을 https:// 하면 된다.
# 1. TLS 프로토콜을 사용하라고 지시 (TLS 1.0, 1.1 또는 1.2 중 서버와 호환되는 버전 자동 선택) curl -1 https://example.com # 2. TLS의 특정버전을 사용하도록 강제 curl --tlsv1.0 https://example.com curl --tlsv1.1 https://example.com curl --tlsv1.2 https://example.com # 3. 인증서의 상태를 확인 (OCSP를 통한 인증서 검증) curl --cert-status -v https://example.com # 4. 보안 연결을 위해 인증서 검증을 건너뛰고 연결 (개발 환경 등에서 유용) curl -k https://example.com curl --insecure https://example.com
Bash
복사

해시 함수

NOTE
해시 함수는 어떤 길이의 데이터를 입력 받아 고정된 길이의 해시 값으로 변환하는 함수입니다.
해시 함수의 특징은 다음과 같습니다.
1.
동일 알고리즘, 동일 입력 데이터이면 결과로 생성되는 값은 같다.
2.
해시 값은 알고리즘이 같다면 길이가 고정된다. 예시로 SHA-256에서의 길이는 256비트이다.
3.
해시 값을 통해 입력 데이터를 유추하기 어렵다. (약한 충돌 내성)
4.
같은 해시 값을 생성하는 다른 2개의 데이터를 찾기 어렵다. 이 문제는 해시 길이가 클수록 더 어려워집니다. (강한 충돌 내성)
해시 값을 테스트하려면 CLI를 통해 테스트할 수 있습니다.
# index.txt를 md5를 통해 암호화 md5 index.txt > MD5 (index.txt) = 5f282c9b5272aa7e69f344d0bf34fa96
Bash
복사

공통 키 암호 / 공개 키 암호 / 디지털 서명

NOTE
TLS의 통신은 암호화된 값으로 상대에게 보낸다음, 복호화하는 흐름으로 진행됩니다. 여기서 암호화하는 방식은 주로 공통 키 방식과 공개 키 방식 2종류가 있습니다.

공통키 방식(대칭 암호화)

공통키 방식은 자물쇠를 잠글때와 열 때 모두 같은 열쇠를 사용하는 방식이다.
서로 동일한 키 사용
통신하는 양쪽 모두 같은 비밀키를 사용하여 데이터를 암호화/복호화 하며 서로간에 키를 교환해야 과정에서 키가 노출될 수 있다는 단점이 있습니다.
TLS에서는 일반 통신의 암호화에 사용합니다.

공개키 방식(비대칭 암호화)

공개키 방식은 자물쇠를 잠글때와 열 때 다른 열쇠를 사용하는 방식이다.
각자 공개/비밀키 쌍을 만들어서 사용
공개 키로 암호화된 데이터는 오직 그에 대응하는 비밀 키로만 복호화가 되며 이는 공개/비밀 키가 서로 수학적으로 연결되어 있기 때문입니다.

디지털 서명

디지털 서명의 경우, 보내는 사람의 개인키로 암호화하고 받는 사람이 공개키로 암호화 했을 때 동일한 것이 나오면 데이터의 무결성이 보장된다.
실제 디지털 서명은 본문 자체를 암호화하는 것이 아닌 해시화하고 그 결과를 암호화한다.

TLS의 암호화 방식

TLS는 공개 키 방식과 공통 키 방식을 함께 사용합니다. 공개 키 방식은 안전성이 높지만 속도가 느리며, 반면에 공통 키 방식은 속도는 빠르나 안전성이 약간 떨어집니다.
TLS에서는 각 통신마다 한 번만 사용되는 공통 키를 생성합니다. 이후, 공개 키 방식으로 키를 전달한 후, 공통 키를 사용하여 빠르게 암호화하는 두 단계를 거칩니다.
package main import ( "crypto/aes" // AES 암호화 알고리즘 패키지 "crypto/cipher" // 고수준 암호화 인터페이스 제공 패키지 "crypto/md5" // MD5 해시 함수 패키지 "crypto/rand" // 암호화를 위한 보안 가능한 난수 생성기 "crypto/rsa" // RSA 암호화 알고리즘 패키지 "io" // 기본 입출력 인터페이스 "testing" // Go의 테스팅 프레임워크 ) // prepareRSA는 RSA 암호화/복호화를 위한 초기 설정을 수행합니다. func prepareRSA() ([]byte, []byte, *rsa.PrivateKey, error) { sourceData := make([]byte, 128) // 암호화할 원본 데이터 label := []byte("") // OAEP 패딩에서 사용할 라벨 (옵션) _, err := io.ReadFull(rand.Reader, sourceData) // 원본 데이터에 대한 무작위 값 생성 if err != nil { return nil, nil, nil, err } privateKey, err := rsa.GenerateKey(rand.Reader, 2048) // 2048비트 RSA 키 생성 if err != nil { return nil, nil, nil, err } return sourceData, label, privateKey, nil } // BenchmarkRSAEncryption은 RSA 암호화의 벤치마크를 측정합니다. func BenchmarkRSAEncryption(b *testing.B) { sourceData, label, privateKey, err := prepareRSA() if err != nil { b.Fatal(err) } publicKey := &privateKey.PublicKey // 공개 키 추출 md5hash := md5.New() // MD5 해시 생성자 b.ResetTimer() for i := 0; i < b.N; i++ { _, err := rsa.EncryptOAEP(md5hash, rand.Reader, publicKey, sourceData, label) if err != nil { b.Error(err) } } } // BenchmarkRSADecryption은 RSA 복호화의 벤치마크를 측정합니다. func BenchmarkRSADecryption(b *testing.B) { sourceData, label, privateKey, err := prepareRSA() if err != nil { b.Fatal(err) } publicKey := &privateKey.PublicKey md5hash := md5.New() encrypted, err := rsa.EncryptOAEP(md5hash, rand.Reader, publicKey, sourceData, label) if err != nil { b.Fatal(err) } b.ResetTimer() for i := 0; i < b.N; i++ { _, err := rsa.DecryptOAEP(md5hash, rand.Reader, privateKey, encrypted, label) if err != nil { b.Error(err) } } } // prepareAES는 AES 암호화/복호화를 위한 초기 설정을 수행합니다. func prepareAES() ([]byte, []byte, cipher.AEAD, error) { sourceData := make([]byte, 128) // 암호화할 원본 데이터 _, err := io.ReadFull(rand.Reader, sourceData) if err != nil { return nil, nil, nil, err } key := make([]byte, 32) // 256비트 AES 키 생성 _, err = io.ReadFull(rand.Reader, key) if err != nil { return nil, nil, nil, err } nonce := make([]byte, 12) // 12바이트 nonce 값 생성 _, err = io.ReadFull(rand.Reader, nonce) if err != nil { return nil, nil, nil, err } block, err := aes.NewCipher(key) // AES 블록 생성 if err != nil { return nil, nil, nil, err } gcm, err := cipher.NewGCM(block) // 갈루아/카운터 모드(GCM) 생성 if err != nil { return nil, nil, nil, err } return sourceData, nonce, gcm, nil } // BenchmarkAESEncryption은 AES 암호화의 벤치마크를 측정합니다. func BenchmarkAESEncryption(b *testing.B) { sourceData, nonce, gcm, err := prepareAES() if err != nil { b.Fatal(err) } b.ResetTimer() for i := 0; i < b.N; i++ { gcm.Seal(nil, nonce, sourceData, nil) } } // BenchmarkAESDecryption은 AES 복호화의 벤치마크를 측정합니다. func BenchmarkAESDecryption(b *testing.B) { sourceData, nonce, gcm, err := prepareAES() if err != nil { b.Fatal(err) } encrypted := gcm.Seal(nil, nonce, sourceData, nil) b.ResetTimer() for i := 0; i < b.N; i++ { _, err := gcm.Open(nil, nonce, encrypted, nil) if err != nil { b.Error(err) } } }
Go
복사
go test -bench . # 공개키 방식 BenchmarkRSAEncryption-8 46796 25309 ns/op BenchmarkRSADecryption-8 1359 878798 ns/op # 공통키 방식 BenchmarkAESEncryption-8 21582248 55.24 ns/op BenchmarkAESDecryption-8 21520191 56.16 ns/op
Bash
복사
벤치마크 결과

키 교환

NOTE
키 교환은 클라이언트 - 서버 사이에 키를 교환하는 방식이며, 단순한 방법으로는 공통키 생성이후 암호화해서 보내는 방식과 키교환 전용 알고리즘인 디피-헬먼 알고리즘을 사용하기도 합니다.
디피-헬먼 알고리즘의 핵심은 키 자체를 교환하는게 아니라, 클라이언트 - 서버에서 각각 키 재료를 만들어 서로 교환하고 각자 계산해서 같은 키를 얻는 것입니다.
# g=5, p=23, Y=6 # Y(서버측 비밀 값) Ys = (g^Y) % p = (5^6)%23 = 23 # X(클라이언트측 비밀 값) Xs = (g^X)%p = (5^15)%23 = 19 # 최종적으로 Xs, Ys를 서로 전달해주면서 공통 키의 시드를 만든다. ZZ = (Ys^X)%p = (8^15)%23 = 2 ZZ = (Xs^Y)%p = (19^6)%23 = 2
Bash
복사
TLS상에서는 서버가 계산에 사용할 값 pq를 준비합니다. 이 값들은 공개 정보로서 그대로 클라이언트에게 넘겨줍니다.
서버 → 클라이언트: p, q, Ys
클라이언트 → 서버: Xs

TLS 통신 절차

NOTE
TLS 통신은 크게 세 가지 단계로 구분됩니다. 첫 번째는 핸드셰이크 프로토콜을 통한 통신 확립, 두 번째는 레코드 프로토콜이라는 통신 단계, 그리고 마지막은 SessionTicket 구조를 이용한 재접속 시의 핸드셰이크입니다.
각 단계를 소개하기 위해 핸드셰이크에서 필요한 2개의 과제가 있습니다.

서버의 신뢰성 확인

서버의 신뢰성을 보증하는 구조는 공개 키를 보증하는 구조이기도 해서, 공개 키 기반구조(PKI)라고 불립니다. 브라우저는 서버에서 그 서버의 SSL 서버 인증서를 가져오는 것에서 시작합니다.
인증서 예시
인증서는 X.509 형식으로 기술된 파일이며, 사이트와 관련된 정보가 있습니다.
인증서는 인증기관(CA)에서 발급해줍니다.
인증서에는 CA의 디지털 서명이 있으며, CA의 인증서의 서명을 검증할 수 있습니다.
신뢰가 확인되지 않은 발행자와 주체가 같은 인증서는 ‘자가 서명 인증서’라고 하며, OpenSSL등의 도구로 쉽게 만들 수 있습니다.
https ssl이 적용 (공인된 CA의 인증서)
https ssl이 적용되지 않음
https ssl이 적용되었는데 사설CA

키 교환과 통신 시작

키 교환의 경우에는 공개 키 암호화를 사용하는 방법과, 키 교환 전용 알고리즘을 사용하는 방식이 있습니다. 어느 쪽을 사용할지는 최초의 Client/Server Hello에서 결정됩니다.
공개 키: 서버 인증서에 첨부된 공개 키로 통신용 공통 키를 암호화해서 서버에 보낸다.
키 교환 알고리즘: 키를 생성할 시드를 클라이언트/서버 양측에서 만들고 시드를 교환해서 계산한 결과가 공통 키가된다.
TLS 1.3부터는 세션 키가 계속 변화하도록 설계되어 키가 노출되더라도 복호화 될 수 없도록 보장하는 전달성의 보안(Foward Security)를 지킵니다.

프로토콜 선택

NOTE
TLS가 제공하는 기능 중 차세대 통신에 가장 중요한점은 애플리케이션 계층 프로토콜을 선택하는 확장 기능입니다.
TLSALPN(Application Layer Protocol Negotiation)을 사용합니다. 최초 핸드셰이크 시, 클라이언트는 이용 가능한 프로토콜 목록을 첨부하여 서버에 보냅니다. 그리고 서버는 키 교환과 인증서, 그리고 선택한 프로토콜을 응답으로 보냅니다.