토큰, 세션, 키 교환과 웹 통신 보안

웹 서비스에서 로그인 이후 사용자를 어떻게 식별할 것인지 생각하다 보면 세션, 토큰, JWT, AES, RSA, Diffie-Hellman 같은 용어가 한꺼번에 등장한다.

처음 보면 서로 비슷한 역할을 하는 것처럼 보이지만 실제로는 담당하는 문제가 다르다.

내가 이해한 기준으로 정리하면 크게 세 가지다.

사용자를 식별하는 방법, 데이터를 암호화하는 방법, 암호화에 사용할 키를 안전하게 공유하는 방법이다.


세션과 토큰

사용자가 로그인을 하면 서버는 해당 사용자가 인증된 상태라는 것을 이후 요청에서도 알아야 한다.

HTTP는 기본적으로 요청마다 독립적이기 때문에 서버 입장에서는 요청을 보낸 사람이 누구인지 별도로 확인할 방법이 필요하다.

가장 단순한 방법이 세션이다.

로그인 -> 서버가 Session 생성 -> Client에 Session ID 전달 -> 이후 요청에서 Session ID 전송 -> 서버가 Session 조회 -> User 식별

서버에는 보통 다음과 같은 정보가 저장된다.

Session ID -> User ID / 만료시간 / 권한 / 기타 상태

클라이언트는 Session ID만 가지고 있고 실제 세션 정보는 서버가 관리한다.

이 방식의 장점은 세션을 서버에서 바로 폐기할 수 있다는 것이다.

예를 들어 로그아웃이나 강제 로그아웃이 발생하면 서버에서 해당 Session을 삭제하면 된다.


세션 토큰은 굳이 암호화할 필요가 없다

예전에 세션을 만들면서 UUID를 AES로 암호화하고 랜덤 IV를 붙여서 클라이언트에 전달하는 방식을 생각해볼 수 있다.

UUID
 -> AES Encryption
 -> IV + Ciphertext
 -> Client

이 방식 자체가 틀린 것은 아니다.

다만 여기서 중요한 것은 암호화와 인증은 다른 문제라는 점이다.

공격자가 암호문을 탈취했다고 가정해보자.

IV + Ciphertext

를 복호화하지 못하더라도 그 값을 그대로 서버에 보내서 인증할 수 있다면 결국 세션 토큰 역할을 한다.

따라서 세션 토큰은 데이터를 숨기는 것보다 예측할 수 없게 만드는 것이 중요하다.

실제로는 충분히 긴 난수를 생성하는 방식이 훨씬 단순하다.

CSPRNG
 -> 256-bit Random Token
 -> Session Token

서버에서는 해당 토큰과 사용자를 연결한다.

Session Token -> User ID

웹 환경이라면 일반적으로 Secure, HttpOnly, SameSite 같은 Cookie 보안 옵션도 함께 고려한다.


HTTPS가 하는 일

여기서 세션과 별개로 또 하나의 문제가 생긴다.

클라이언트가 서버에 세션 토큰을 보내는 과정 자체가 안전해야 한다.

HTTP에서 그냥 전송하면 네트워크를 감청할 수 있는 공격자가 데이터를 볼 수 있다.

Client -> HTTP -> Server

이 문제를 해결하는 것이 HTTPS다.

HTTPS는 단순히 데이터를 AES로 암호화하는 기능이 아니다.

TLS Handshake 과정에서 통신 당사자끼리 암호화에 사용할 키를 안전하게 합의하고, 서버의 신원을 인증한 다음 실제 데이터를 대칭키 암호화로 전송한다.

현대적인 TLS 1.3에서는 일반적으로 ECDHE 계열의 키 교환이 사용된다.

개념적으로 보면 다음과 같다.

Client <-> Server
        ECDHE
          ->
     Shared Secret
          ->
        HKDF
          ->
     Session Keys
          ->
   AES-GCM / ChaCha20-Poly1305

Diffie-Hellman은 AES 키를 직접 보내는 것이 아니다

여기서 자주 헷갈리는 부분이 있다.

Diffie-Hellman은 AES 키 자체를 상대방에게 전달하는 방식이 아니다.

양쪽이 서로 공개값을 주고받은 다음 동일한 공유 비밀값을 각각 계산하는 방식이다.

개념적으로:

Client                         Server

Private A                     Private B
Public A  ----------------->  Public B
Public B  <-----------------  Public A

        Shared Secret 계산
               ↓
        양쪽에서 동일한 값
               ↓
             KDF
               ↓
          AES Key 생성

따라서 실제로는:

ECDHE
 -> Shared Secret
 -> HKDF
 -> AES Key

와 같은 흐름으로 이해하는 것이 정확하다.


그렇다면 HTTPS 없이 직접 구현하면 되는가

기술적으로는 가능하다.

ECDH
 -> Shared Secret
 -> KDF
 -> AES-GCM
 -> 암호화 통신

하지만 이것만 구현한다고 HTTPS와 동일한 보안이 되는 것은 아니다.

가장 중요한 문제가 중간자 공격이다.

공격자가 통신 중간에 끼어들면:

Client <-> Attacker <-> Server

구조를 만들 수 있다.

클라이언트는 공격자와 키를 만들고 서버 역시 공격자와 다른 키를 만들게 할 수 있다.

그래서 실제 TLS는 단순한 키 교환 프로토콜이 아니다.

서버 인증, 인증서 검증, 핸드셰이크 무결성, 키 파생, 암호화 알고리즘, 재전송 방지 등 여러 요소가 같이 들어간다.

결국 실무에서는 직접 ECDH + AES 프로토콜을 만드는 것보다 검증된 TLS를 사용하는 것이 훨씬 안전하다.


AES는 데이터를 보호하고 RSA 같은 비대칭 암호는 다른 역할을 한다

대칭키와 비대칭키의 차이도 여기서 정리할 수 있다.

AES는 같은 키를 사용한다.

AES Key
 -> Encrypt
 -> Ciphertext

AES Key
 -> Decrypt
 -> Plaintext

속도가 빠르기 때문에 실제 대량의 데이터를 암호화하는 데 적합하다.

반면 RSA 같은 비대칭 암호는 공개키와 개인키를 사용한다.

Public Key
Private Key

두 키의 역할이 다르다.

특히 현대 시스템에서는 RSA를 데이터 전체를 암호화하는 용도로 사용하는 것보다 전자서명에서 만나는 경우가 많다.


전자서명

전자서명의 목적은 암호화와 다르다.

암호화의 목적은:

데이터를 다른 사람이 볼 수 없게 한다.

전자서명의 목적은:

데이터가 변조되지 않았는지 확인한다.
누가 서명했는지 확인한다.

RSA 기반 JWT 서명을 예로 들면 다음과 같다.

Header + Payload
       ↓
    SHA-256
       ↓
Private Key로 서명
       ↓
Signature

검증하는 쪽에서는 서버의 Public Key를 이용한다.

Header + Payload
       +
Signature
       ↓
Public Key로 검증

여기서 중요한 점은 Private Key는 비밀이어야 하지만 Public Key는 공개해도 된다는 것이다.

Public Key를 알고 있다고 해서 Private Key를 알아낼 수 있는 것은 아니며, Public Key를 이용해 새로운 정상 서명을 생성할 수도 없다.


JWT

JWT도 이 개념 위에서 보면 어렵지 않다.

일반적인 JWT는:

Header.Payload.Signature

구조다.

예를 들어 RS256을 사용하면:

Header
+
Payload
    ↓
Signature 생성
    ↓
Private Key

그리고 다른 서버나 클라이언트는 Public Key를 이용해 Signature를 검증할 수 있다.

따라서 JWT의 Payload는 기본적으로 암호화된 데이터가 아니다.

Base64URL로 인코딩되어 있기 때문에 내용을 디코딩해서 볼 수 있다.

JWT
 -> Header
 -> Payload
 -> Signature

JWT에서 RS256은 RSA와 SHA-256을 이용한 서명 방식이고, HS256은 RSA가 아니라 HMAC-SHA256을 사용하는 대칭키 기반 방식이다.

이 차이는 상당히 중요하다.

RS256
 -> Private Key로 서명
 -> Public Key로 검증

HS256
 -> Secret Key로 서명
 -> 동일한 Secret Key로 검증

JWT와 세션의 차이

세션과 JWT는 둘 다 인증 상태를 전달하는 데 사용할 수 있지만 구조가 다르다.

전통적인 세션은:

Client
 -> Session ID
 -> Server
 -> Session Store
 -> User 정보 조회

JWT는:

Client
 -> JWT
 -> Signature 검증
 -> Payload 확인
 -> User 식별

JWT는 토큰 자체에 필요한 정보를 넣을 수 있기 때문에 서버 측 세션 저장소에 대한 의존성을 줄일 수 있다.

반대로 서버에서 세션을 직접 관리하는 방식은 세션을 즉시 폐기하거나 상태를 변경하기 쉽다.

따라서 JWT가 무조건 세션보다 좋은 것도 아니고, 세션이 무조건 더 좋은 것도 아니다.

시스템의 요구사항에 따라 선택하면 된다.


토큰 보안에서 가장 중요한 것은 암호화가 아니다

인증 토큰을 설계할 때 가장 흔한 착각은 다음과 같다.

UUID
 -> AES
 -> 안전한 토큰

하지만 실제로 중요한 것은 예측 불가능성, 탈취 방지, 만료, 폐기, 재사용 방지다.

예를 들어 256비트 난수로 생성한 토큰이라면 공격자가 정상적인 토큰을 추측하는 것은 현실적으로 어렵다.

CSPRNG
 -> 256-bit Random
 -> Session Token

그리고 HTTPS가 토큰을 전송하는 구간을 보호한다.

Session Token
      ↓
HTTPS / TLS
      ↓
Server

여기에 적절한 만료와 폐기 정책을 추가한다.

Random Token
 -> HTTPS
 -> Expiration
 -> Revocation
 -> Secure Storage

이것이 실제 세션 보안에서 중요한 부분이다.


전체 구조를 한 번에 보면

웹 인증과 통신 보안을 하나의 흐름으로 묶으면 다음과 같다.

Login
 -> Authentication
 -> Session / Token 발급
 -> HTTPS를 통한 전달
 -> 이후 요청에서 Token 전송
 -> Server에서 검증
 -> User 식별

HTTPS 내부에서는:

TLS Handshake
 -> Server Authentication
 -> ECDHE
 -> Shared Secret
 -> HKDF
 -> AES-GCM / ChaCha20-Poly1305
 -> Encrypted Communication

JWT를 사용한다면:

Header + Payload
 -> Signature
 -> Private Key
 -> JWT 발급
 -> Client 전달
 -> Public Key로 Signature 검증

각 기술의 역할을 구분하면 복잡할 것이 없다.

Session / Token
 -> 사용자를 식별하기 위한 인증 수단

ECDHE
 -> 암호화에 사용할 키를 안전하게 합의

HKDF
 -> 공유 비밀에서 실제 사용할 키를 파생

AES-GCM
 -> 실제 데이터를 암호화하고 무결성을 검증

RSA / ECDSA / EdDSA
 -> 전자서명과 인증에 사용

TLS
 -> 위 요소들을 조합해 안전한 통신 채널 제공

결국 보안 시스템은 하나의 암호화 알고리즘으로 해결하는 것이 아니다.

인증과 세션 관리, 키 교환, 데이터 암호화, 무결성 검증, 서버 인증이 각각 다른 문제이고, TLS 같은 프로토콜은 이 문제들을 하나의 검증된 통신 구조로 묶어놓은 것이다.