Sec / TLS
TLS 1.3 Handshake와
Record Protocol
브라우저가 받은 JSON을 다른 서비스에 그대로 제출해도 될까요?
은행에서 받은 잔액 JSON에는 어떤 확인을 거쳤다는 정보가 남을까요? 응답을 받기 전으로 돌아가, 아직 보내지 않은 잔액 조회 요청부터 따라가겠습니다.
요청이 출발하기까지GET /…/balances 은행에 잔액을 조회합니다.인증서 기반 TLS 1.3 0-RTT와 세션 재개는 제외합니다.
가상 금융 API 실제 요청이나 암호 계산은 실행하지 않습니다.
요청은 준비됐지만, 아직 보내지 않습니다
가상 은행 api.bank.example의 잔액 조회 API를 호출하려고 합니다. 브라우저는 URL에서 서버 이름과 요청 경로를 읽고, DNS로 IP 주소를 찾고, TCP 연결을 준비합니다. 요청을 보내려면 상대가 은행 서버인지 확인하고 암호화에 쓸 키도 마련해야 합니다. 이 연결 준비 절차가 TLS Handshake입니다. 여기서는 0-RTT와 클라이언트 인증서를 사용하지 않는, TCP 위의 인증서 기반 TLS 1.3 연결을 다룹니다.
- URL요청할 이름과 경로
- DNS연결할 IP 주소
- TCP바이트를 주고받을 연결
예시 URL 살펴보기
DNS 결과와 TLS 서버 이름은 같은 검사가 아니다
브라우저는 URL에서 api.bank.example이라는 서비스 이름을 읽습니다. DNS는 그 이름으로 연결할 IP 주소를 찾고, TCP는 해당 주소와 바이트를 주고받을 연결을 만듭니다. 두 단계만으로 상대가 기대한 서비스인지는 확정되지 않습니다.
| 단계 | 답하는 질문 | 답하지 않는 질문 |
|---|---|---|
| DNS | 어느 주소로 연결할 것인가? | 그 주소의 서버가 api.bank.example인가? |
| TCP | 그 주소와 전송 연결이 열렸는가? | 서버가 제시할 인증서를 받아들여도 되는가? |
| TLS 서비스 이름 검사 | 인증서 식별자가 요청한 이름과 맞는가? | 저장 파일이 이후 수정되지 않았는가? |
TLS 클라이언트는 URL에서 얻은 api.bank.example을 기준 식별자로 삼고 인증서에 제시된 식별자와 대조합니다. DNS가 돌려준 IP 주소와 인증서 이름을 같은 값처럼 비교하는 절차가 아닙니다.
연결을 준비하고,
첫 요청을 보냅니다.
주황색 메시지를 따라가면, 어떤 준비가 끝났고 무엇이 아직 남았는지 보입니다.
연결 조건부터 맞춥니다
잔액 조회 요청을 보내기 전에, 브라우저와 서버는 이번 연결에 쓸 TLS 버전과 암호화 방식을 정합니다.
브라우저는 ClientHello에 지원하는 조건과 key_share를 담아 보내고, 서버는 선택한 조건과 자신의 key_share를 ServerHello로 돌려줍니다. 이때 교환하는 것은 키 합의에 쓸 공개 값이지, 완성된 암호화 키가 아닙니다.
양쪽은 각자 보관한 비밀값과 받은 공개 값으로 공유 비밀값을 계산하고, Handshake 메시지를 암호화할 키를 따로 만듭니다. 아직 상대가 은행인지는 확인하지 않았습니다.
HTTP 요청은 브라우저에 남아 있습니다. 먼저 오간 것은 연결 조건과 키 합의에 쓸 값입니다.
- 브라우저 → 서버 ClientHello
- 서버 → 브라우저 ServerHello
ClientHello에 들어가는 메시지 이름과 역할
ClientHello는 클라이언트가 연결 조건을 먼저 제안하는 메시지입니다. 서버가 고를 수 있는 TLS 버전과 암호군을 알리고, 키 합의에 필요한 key_share와 연결에 필요한 확장을 싣습니다.
| 항목 | 역할 |
|---|---|
| supported_versions | 클라이언트가 지원하는 TLS 버전 제안 |
| cipher_suites | 사용할 수 있는 AEAD와 hash 조합 제안 |
| key_share | 서버와 공유 secret을 만들 공개 키 재료 전달 |
| server_name | 연결하려는 서비스 이름 전달 |
GET 경로, Authorization 헤더, JSON 본문은 이 메시지의 항목이 아닙니다. 따라서 ClientHello를 관찰할 수 있더라도, 그 사실만으로 보호될 HTTP 요청과 응답까지 읽을 수 있는 것은 아닙니다.
TLS 1.3 키 파생 절차를 식 없이 읽기
ServerHello가 서버의 선택과 key_share를 돌려주면 양쪽은 같은 공유 비밀값(shared secret)을 계산할 재료를 갖습니다. TLS 1.3 key schedule은 이 재료와 handshake 기록을 섞어 단계별 비밀값을 파생합니다.
- 01 key_share
- 02 공유 비밀값
- 03 handshake 트래픽 비밀값
- 04 Finished
- 05 애플리케이션 트래픽 비밀값
handshake 메시지와 애플리케이션 데이터는 서로 다른 단계의 비밀값으로 보호됩니다. 보내는 방향과 받는 방향의 트래픽 비밀값(traffic secret)도 나뉩니다. record를 처리할 때는 해당 단계와 방향에 맞는 키를 사용합니다.
지금 연결한 곳이 은행인지 확인합니다
암호화에 쓸 키를 준비한 뒤에는, 연결한 상대가 api.bank.example의 서버인지 확인합니다.
서버는 EncryptedExtensions로 추가 연결 설정을 알리고 Certificate로 인증서 체인을 보냅니다. 이 메시지들과 뒤이은 CertificateVerify는 이미 Handshake용 키로 암호화합니다.
브라우저는 인증서 체인과 유효 기간, 요청한 서버 이름을 검사하고 CertificateVerify의 서명도 확인합니다. 현재까지의 Handshake 기록과 인증서 공개키로 서명을 검사해, 상대가 대응하는 개인키를 보유하는지 확인합니다.
인증서에 적힌 이름이 요청한 서버와 맞지 않으면 여기서 연결을 중단합니다. 잔액 조회 요청은 보내지 않습니다.
- 브라우저 → 서버 ClientHello
- 서버 → 브라우저 ServerHello
- 서버 → 브라우저 EncryptedExtensions
- 서버 → 브라우저 Certificate
- 서버 → 브라우저 CertificateVerify
인증서의 이름이 다르면?
요청 이름은 api.bank.example인데 제시된 인증서가 다른 이름만 식별하면, 클라이언트는 그 인증서를 이 서비스의 신원 근거로 받아들이지 않습니다.
연결 중단. HTTP 요청은 보내지 않습니다.
인증서 경로와 서비스 이름 확인
서버 인증에는 서로 다른 검사가 이어집니다. 인증서 체인을 신뢰할 수 있는지, 인증서의 유효 기간과 용도가 맞는지, 제시된 식별자가 api.bank.example과 맞는지를 확인합니다.
인증서 체인과 이름이 유효한지 검사하는 것과, 인증서의 개인키를 실제로 가진 상대가 이번 연결에 참여했는지 검사하는 것은 별개입니다. CertificateVerify는 서버가 해당 개인키로 현재까지의 handshake 기록에 서명했는지 확인합니다.
| 검사 | 통과해도 아직 남는 질문 |
|---|---|
| 인증서 경로 | 이 인증서가 api.bank.example용인가? |
| 서비스 이름 | 상대가 인증서의 개인키를 갖고 있는가? |
| CertificateVerify | Finished 검증으로 handshake 무결성과 키 확인까지 끝났는가? |
주고받은 기록과 키를 확인합니다
서버 인증을 통과한 뒤에는, 양쪽의 Handshake 기록과 계산한 키가 맞는지도 확인합니다.
서버가 먼저 Finished를 보내면 브라우저는 자신이 기록한 Handshake 메시지와 파생 키로 확인 값을 다시 계산해 비교합니다. 값이 맞으면 브라우저도 자신의 Finished를 보내고, 서버가 같은 방식으로 검사합니다.
계산한 값과 받은 값이 다르면 연결을 중단합니다. Finished는 그 시점까지의 Handshake 기록을 검사합니다. 앞으로 받을 잔액 JSON은 검사 대상이 아닙니다.
브라우저는 서버의 Finished를 확인하고 자신의 Finished를 보낸 뒤 HTTP 요청을 전송합니다.
- 브라우저 → 서버 ClientHello
- 서버 → 브라우저 ServerHello
- 서버 → 브라우저 EncryptedExtensions
- 서버 → 브라우저 Certificate
- 서버 → 브라우저 CertificateVerify
- 서버 → 브라우저 Finished
- 브라우저 → 서버 Finished
Finished가 일치하지 않으면?
계산한 verify_data가 수신값과 다르면 handshake를 계속하지 않습니다. 보호된 API 요청도 이 연결로 보내지 않습니다.
연결 중단. HTTP 요청은 보내지 않습니다.
Finished가 묶는 handshake 기록
Finished의 verify_data는 finished_key와 그 시점까지 쌓인 handshake 기록으로 계산합니다. 수신자는 자신이 본 기록으로 값을 다시 계산해 받은 값과 비교합니다.
| 확인하는 것 | 확인하지 않는 것 |
|---|---|
| 양쪽이 같은 handshake 기록을 보았는가 | 앞으로 받을 HTTP 응답의 내용 |
| 해당 단계의 handshake 비밀값을 보유하는가 | 저장한 JSON 파일의 이후 변경 |
| 인증 메시지를 포함한 기록과 파생 키가 같은 handshake에 묶였는가 | 제3자가 사본의 출처를 검증할 수 있는가 |
handshake 메시지가 중간에서 바뀌었거나 양쪽이 다른 키를 계산했다면 verify_data가 맞지 않습니다. 클라이언트는 이 연결로 보호된 API 요청을 보내기 전에 handshake를 실패로 처리합니다.
이제 잔액 조회 요청이 전송됩니다
브라우저 안에 남아 있던 HTTP 요청이 TLS 연결을 따라 서버로 이동합니다.
TLS는 HTTP 데이터를 record 단위로 나누어 암호화하고 변조를 탐지할 인증 태그를 붙입니다. 앞서 쓴 Handshake용 키와는 별도로 application traffic key를 사용하며, 보내는 방향과 받는 방향의 키도 구분합니다.
서버는 record를 검사하고 복호화한 뒤 HTTP 요청을 읽습니다. 하나의 HTTP 메시지와 TLS record, 네트워크 패킷이 각각 일대일로 대응하지는 않습니다.
요청 경로와 Authorization 헤더는 암호화된 record 안에 있습니다. 화면의 화살표 하나가 실제 패킷 하나를 뜻하지는 않습니다.
- 브라우저 → 서버 ClientHello
- 서버 → 브라우저 ServerHello
- 서버 → 브라우저 EncryptedExtensions
- 서버 → 브라우저 Certificate
- 서버 → 브라우저 CertificateVerify
- 서버 → 브라우저 Finished
- 브라우저 → 서버 Finished
- 브라우저 → 서버 TLS application data
HTTP 요청 원문
GET /open-banking/v4.0/aisp/accounts/acct_demo_7F21/balances HTTP/1.1
Host: api.bank.example
Authorization: Bearer <redacted>
X-FAPI-Interaction-ID: trace_tls13_001
Accept: application/jsonTLS record와 네트워크 패킷을 구분하기
TCP는 순서 있는 바이트 흐름을 제공할 뿐 메시지 경계를 보존하지 않습니다. TLS record 경계는 TCP 세그먼트나 IP 패킷 경계와 독립적입니다.
- 01 HTTP 바이트
- 02 TLS record A + B
- 03 TCP 바이트 흐름
- 04 IP 패킷
- 05 다시 모은 record
큰 record 하나가 여러 패킷에 나뉠 수 있고, 작은 record 여러 개가 같은 전송 구간에 실릴 수도 있습니다. 이 때문에 패킷 캡처의 한 줄을 하나의 TLS record나 HTTP 메시지와 곧바로 대응시키면 실제 경계를 잘못 읽게 됩니다.
전송 중 변조가 탐지되는 지점
TLS 1.3은 AEAD로 record 내용을 암호화하고, record header를 추가 인증 데이터로 포함해 authentication tag(인증 태그)를 계산합니다. 수신자는 방향과 순서에 맞는 traffic key와 nonce로 인증 태그를 검증한 뒤 record를 복호화합니다.
| 변경 | TLS record 검사 | 결과 |
|---|---|---|
| 전송 중 암호문(ciphertext)이나 인증 태그(authentication tag) 변경 | 실패 | 애플리케이션에 넘기지 않고 연결 오류 처리 |
| 정상 복호화 뒤 브라우저 메모리에서 변경 | 이미 완료 | 애플리케이션 보안 영역에서 다룸 |
| 저장한 JSON 파일을 나중에 변경 | 적용되지 않음 | 파일만으로 원본 여부를 판정할 수 없음 |
수신자는 인증에 실패한 record를 폐기해 변조된 데이터를 애플리케이션에 넘기지 않습니다. 전송 중 무결성은 이 인증 검사로 확인합니다.
브라우저가 JSON을 읽는 순간
브라우저가 서버의 record 인증을 검증하고 복호화하면 애플리케이션은 원래 HTTP 응답과 JSON을 읽을 수 있습니다.
HTTP/1.1 200 OK
Content-Type: application/json
X-FAPI-Interaction-ID: trace_tls13_001
{
"Data": {
"Balance": [
{
"AccountId": "acct_demo_7F21",
"Amount": {
"Amount": "72840000.00",
"Currency": "KRW",
"SubType": "BCUR"
},
"CreditDebitIndicator": "Credit",
"Type": "ITAV",
"DateTime": "2026-08-25T10:14:32+09:00",
"CreditLine": [
{
"Included": false,
"Amount": {
"Amount": "10000000.00",
"Currency": "KRW",
"SubType": "BCUR"
},
"Type": "Pre-Agreed"
}
]
}
]
},
"Links": {
"Self": "https://api.bank.example/open-banking/v4.0/aisp/accounts/acct_demo_7F21/balances"
},
"Meta": {
"TotalPages": 1
}
}복호화 뒤 보호 경계가 바뀌는 순간
record 인증과 복호화가 끝나면 브라우저는 HTTP 상태줄, 헤더, 본문을 평문(plaintext)으로 애플리케이션에 넘깁니다. fetch 응답, 개발자 도구, 로그, 캐시, 다운로드 파일은 이 시점부터 브라우저와 애플리케이션의 권한 아래 놓입니다.
| 위험 | TLS가 계속 막는가 | 다루는 곳 |
|---|---|---|
| 네트워크에서 record 변조 | 예 | TLS record 인증 |
| 브라우저 확장이나 스크립트의 평문 접근 | 아니요 | 브라우저 권한과 애플리케이션 보안 |
| 다운로드 뒤 파일 수정 | 아니요 | 별도 서명·저장소 무결성·출처 확인 방식 |
저장 사본을 다른 사람에게 제출하려면 원본 서버와 응답 바이트를 함께 확인할 별도 자료가 필요합니다. TLS 연결이 성공했다는 브라우저 화면이나 파일 해시(hash)만으로는 어느 서버 세션에서 온 내용인지까지 이어지지 않습니다.
응답이 파일이 되면, 무엇이 남을까요?
서버의 잔액 응답도 TLS record로 돌아오고, 브라우저가 검사와 복호화를 마치면 JSON을 읽습니다. 다만 그 JSON을 저장한다고 은행 서버의 서명이 자동으로 붙지는 않습니다. 이제 응답을 받은 브라우저와 파일만 전달받은 서비스가 각각 무엇을 확인할 수 있는지 비교해 보겠습니다.
{
"AccountId": "acct_demo_7F21",
"Amount": { "Amount": "92840000.00", "Currency": "KRW" },
"CreditDebitIndicator": "Credit",
"Type": "ITAV"
}HTTPS가 보장하는 범위를 문장으로 자르기
TLS 1.3은 클라이언트와 서버가 통신하는 동안 상대 서버를 인증하고 record의 기밀성과 무결성을 지킵니다. 브라우저는 이 연결의 키와 상태를 바탕으로 받은 데이터를 검사합니다.
| 위치 | 남아 있는 확인 근거 | 제3자가 파일만 받았을 때 |
|---|---|---|
| 진행 중인 TLS 연결 | handshake 상태와 트래픽 키 | 연결 당사자가 아님 |
| 브라우저가 복호화한 뒤 | 애플리케이션이 읽는 평문 | TLS session과 자동 결합되지 않음 |
| 저장한 JSON 파일 | 파일 내용과 별도로 붙인 metadata | 원본 서버 출처를 파일만으로 판정할 수 없음 |
원본과 수정 사본을 나란히 재생하기
첫 번째 경로에서는 공격자가 전송 중인 암호문(ciphertext)을 바꿉니다. record 인증이 실패하므로 브라우저는 수정된 내용을 JSON으로 내놓지 않습니다. 두 번째 경로에서는 브라우저가 72,840,000원을 정상 수신한 뒤 저장 파일의 값을 92,840,000원으로 고칩니다.
| 사례 | 발생 시점 | TLS 결과 | 파일만 받은 제3자 |
|---|---|---|---|
| ciphertext 변조 | record 수신 전 | 인증 실패 | 수정된 JSON이 만들어지지 않음 |
| 72,840,000원 → 92,840,000원 | 정상 복호화 후 | 이미 성공 | 원본과 수정본을 구분할 TLS 상태가 없음 |
| 정상 JSON 그대로 복사 | 정상 복호화 후 | 이미 성공 | 내용이 맞아도 서버 출처를 파일만으로 재검증할 수 없음 |
기술 출처
Handshake 메시지의 순서와 역할은 아래 규격을 기준으로 설명합니다.
- 도입과 전체 흐름RFC 8446 §2. Protocol Overview ↗
- ClientHello와 ServerHelloRFC 8446 §4.1. Key Exchange Messages ↗
- 인증서와 FinishedRFC 8446 §4.4. Authentication Messages ↗
- 서비스 이름 확인RFC 9525. Service Identity in TLS ↗
- HTTP 데이터와 TLS recordRFC 8446 §5. Record Protocol ↗
- Handshake 키와 application 키RFC 8446 §7.1. Key Schedule ↗