TLS/SSL (feat. CA 인증서)...
최근 회사에서 로컬 PC에 개발 환경을 세팅하던 중, Spring 서버의 의존성을 받아오는 과정에서 TLS 에러를 만났다.
원인을 따라가 보니, JVM에 회사 CA 인증서를 등록해 주지 않아서 사내 SSL Inspection Proxy를 신뢰하지 못해 발생한 문제였다.
결국 JVM에 회사 CA 인증서를 세팅해 주는 것으로 해결됐다. ( 내 PC에는 CA 인증서 존재 )

GPT 잘 그리네...
문제 자체는 인증서 등록 한 번으로 끝났지만, "왜 JVM이 이 프록시를 못 믿었을까?"라는 궁금증이 남았다.
그 의문이 자연스럽게 CA에 대한 공부로 이어졌고, CA를 이해하려다 보니 결국 그 토대가 되는 TLS까지 정리하는 편이 낫겠다 싶어 이렇게 글로 남긴다.
기존 local에 CA 인증서가 있는데 왜 JVM에 별도로 CA 인증서를 설치해야 하는가? 답은 Java의 철학에 있다.
Java의 핵심 철학은 플랫폼 독립성(Write Once, Run Anywhere) 이다.
OS마다 신뢰 저장소 관리 방식이 제각각이다. (Windows는 레지스트리, Mac은 키체인, Linux는 /etc/ssl/certs를 사용)
JVM이 OS 신뢰 저장소를 직접 참조하면 실행 환경에 따라 동작이 달라지고, 이는 "어떤 플랫폼에서든 동일하게 동작한다" 는 Java의 핵심 철학에 정면으로 어긋난다.
그래서 Java는 OS와 무관하게 동일한 동작을 보장하기 위해 cacerts라는 자체 신뢰 저장소를 사용한다.
TLS란?
TLS(Transport Layer Security)는 인터넷 통신에서 암호화, 인증, 무결성을 보장하는 보안 프로토콜입니다.
HTTP는 TCP 연결만 맺으면 상대 검증 없이 평문으로 통신하는데, HTTPS는 TCP 연결 후 TLS Handshake를 추가로 수행해 이 세 가지를 해결합니다.
- 암호화 — 세션키(대칭키)로 통신 내용을 암호화해 제3자가 읽지 못하게 합니다.
- 인증 — CA가 서명한 인증서로 상대 서버가 진짜인지 검증합니다.
- 무결성 — MAC으로 전송 중 데이터가 변조되지 않았음을 보장합니다.
SSL은 TLS의 구버전으로, 현재는 TLS로 대체됐지만 관습적으로 SSL이라 부르는 경우가 많습니다.
TLS Handshake 과정
목적은 명확하다. 서버 인증, 암호 알고리즘 협상, 세션키(대칭키) 생성이다. 큰 흐름은 다음과 같다.
- ClientHello — 클라이언트가 지원하는 TLS 버전, 암호화 알고리즘 목록, 그리고 랜덤값을 보낸다.
- ServerHello — 서버가 그중 사용할 버전과 암호화 알고리즘을 골라 응답하고, 자신의 랜덤값을 보낸다.
- Certificate — 서버가 자신의 (CA)인증서를 보낸다. 클라이언트는 이걸로 상대가 신뢰할 수 있는 서버인지 검증한다.
- ( 네트워크 x) Key Exchange — 각 임시 공개키를 활용해 비밀값(Shared Secret)을 조합해 각자 내부에서 생성( 공개키 위/변조가 없으면 server/client 가각에서 생성한 값은 동일하다.)
- ( 네트워크 x)Key Expansion — Shared Secret 값 + Client 랜덤값 + Server 랜덤값 조합으로 Master Secret 생성 이를통해 최종적으로 대칭키(암호화키) 생성
- Finished — 양쪽이 핸드셰이크가 정상적으로 끝났음을 확인하고, 이제부터 암호화된 통신을 시작한다.
CA란 (인증)?
CA(Certificate Authority, 인증 기관) 는 인증서를 발급해 주는 신뢰할 수 있는 제3자다.
서버가 "나는 진짜 example.com이다"라고 주장할 때, 이를 보증해 주는 공신력 있는 기관이라고 보면 된다.
우리 PC와 브라우저는 이미 신뢰할 만한 CA들의 인증서를 신뢰 저장소(trust store) 에 가지고 있고, 이를 기준으로 서버 인증서의 진위를 판단한다.
인증서 체인 (Certificate Chain)
인증서는 보통 Leaf(서버 인증서) → 중간 CA → Root CA 로 이어지는 사슬 구조를 갖는다.
검증은 이 사슬을 따라 올라가며 이루어지고, 최종적으로 신뢰 저장소에 있는 Root에 도달하면 신뢰가 성립한다.
(참고로 사내 프록시 같은 환경에서는 중간 CA 없이 Leaf → Root로 바로 인증하는 경우도 있다.)
그렇다면 중간 CA는 왜 둘까? 만약 중간 CA가 없다면, Root CA가 직접 자신의 비공개 키를 사용해 수많은 인증서를 발급·갱신해야 한다. 이는 업무 부하 측면에서도 비효율적이고, 무엇보다 가장 공신력 있는 Root의 비공개 키를 일상적으로 꺼내 쓰는 셈이라 보안상 위험하다. 그래서 Root는 평소에 오프라인으로 안전하게 보관하고, 실무적인 발급은 중간 CA에 위임한다.
중간 CA 키가 유출돼도 피해 범위를 그 CA로 한정할 수 있다는 장점도 있다.
인증서의 구조와 검증 절차
인증서에는 핵심적으로 다음 항목이 들어간다.
- Subject — 인증서의 주인 (예: 서버의 도메인)
- Issuer — 인증서를 발급한 기관 (CA)
- Public Key — 주인의 공개키
- Signature — 발급 기관이 자신의 개인키(private key) 로 서명한 값
* S = Server 인증서, C = CA 인증서
* 예시는 가장 간단한 인증서 체인
STEP1 -> Server(Subject)가 Client에게 Leaf 인증서(S)를 제공한다.
STEP2 -> Client는 local trust storage에서 Issuer의 인증서(C)가 있는지 찾는다.
STEP3 -> Issuer의 인증서(C)가 존재한다면 해당 인증서(C)의 공개키를 활용해 Signature를 검증한다.
STEP4 -> 해당 Issuer가 Root CA면 검증을 마친다.
TLS 세션키 생성 ( 암호화 및 무결성 보장)
TLS Handshake의 Key Exchange -> Key Expand 단계를 거쳐 양쪽은 세 가지 값을 조합해 세션키(대칭키)를 만들어 Master Secret을 생성하고 최종적으로 세션키(대칭키,암호화키)를 만든다.
Master Secret 생성 요소
- 클라이언트 랜덤 — ClientHello에서 평문으로 전송
- 서버 랜덤 — ServerHello에서 평문으로 전송
- Shared Secret(=Pre-Master Secret) — 키 교환으로 공유하는 값
Shared Secret 생성 방법
- client와 server 각각 임시 개인키/공개키 생성
- client와 server 자신의 공개키 교환
- client와 server 각각 자신의 개인키*상대 공개키를 계산해서 Shared Secret을 만든다.
( 계산을 통해 위/변조가 없다면 양측에서 생성한 Shared Secret 값은 동일하고 이는 네트워크로 절대 전송하지 않는다.)
Shared Secret, Master Secret, Session Key 상세 생성 알고리즘은 설명은 복잡해서 생략한다.
또한 대칭키만으로 무결성을 어떻게 보장하는지도 생략한다.
현재 설명은 TLS1.3 기준으로 작성
HTTP와 HTTPS에 대한 간단한 정리...
HTTP는 왜 위험한가
HTTP는 TCP 3-way handshake로 연결만 수립하고 곧바로 통신을 시작한다.
즉 상대가 누구인지 검증하는 과정도 없고, 주고받는 내용을 암호화하지도 않는다.
그래서 GET이든 POST든 패킷을 들여다보면 본문이 그대로 보인다.
POST가 GET 보다 안전하다는 것은 URL에 데이터가 노출되지 않는다는 의미이지, POST 자체가 암호화를 제공한다는 뜻은 아니다. 실제 데이터 암호화는 HTTPS(TLS)가 담당한다.
HTTPS와 TLS
HTTPS는 이런 HTTP의 보안 취약점을 보완하기 위해 TLS(Transport Layer Security) 프로토콜을 사용한다. 동작 순서는 이렇다. 먼저 TCP 3-way handshake로 Connection이 수립되고(Connection Established), 그 위에서 TLS Handshake를 진행한다.
SSL은 TLS의 구버전이다. 지금은 보안 문제로 사실상 쓰이지 않고 TLS로 대체됐지만, 이름의 관성 때문에 여전히 "SSL"이라고 부르는 경우가 많다.
HTTPS에서도 보이는 것 vs 숨겨지는 것
"어떤 사이트에 접속했는지"까지는 추정 가능해도, "그 사이트에서 정확히 어떤 페이지를 어떤 데이터로 요청했는지"는 알 수 없다.
보이는 것
- IP, Port, 도메인 이름, 통신량, 통신 시간 등
안보이는것
- URL의 경로(path)나 쿼리, 요청/응답 본문