Chapter 3

식별, 인가, 보안

  • 3.1 클라이언트 식별과 쿠키
  • 3.2 기본 인증
  • 3.3 다이제스트 인증
  • 3.4 보안 HTTP(HTTPS)

HTTP는 원래 무상태(stateless) 프로토콜이야. 요청 간에 사용자를 기억하지 못하지. 근데 쇼핑몰은 장바구니를 유지해야 하고, 맞춤 콘텐츠를 보여주려면 사용자가 누군지 알아야 하잖아. 그리고 보호된 리소스에는 아무나 접근하면 안 되니까 인증이 필요하고, 네트워크를 오가는 데이터가 도청당하면 안 되니까 암호화도 필요해. 이 장에서는 사용자를 식별하고, 인증하고, 통신을 안전하게 만드는 방법을 다뤄보자.

웹 서버가 사용자를 식별하려는 이유는 개인화된 인사, 맞춤 추천, 저장된 사용자 정보(배송 주소, 카드 정보), 세션 추적(장바구니 유지) 같은 거야. HTTP 자체는 이런 기능을 직접 제공하지 않으니까 여러 우회적 방법이 발전해왔지.

HTTP 요청 헤더에서 약간의 사용자 정보를 얻을 수 있긴 해. From은 이메일 주소인데 스팸 문제로 거의 아무도 안 보내고, User-Agent는 브라우저 종류를 알려주지만 특정 사용자를 식별하는 데는 못 쓰고, Referer는 이전 페이지 URL인데 탐색 경로 추적 정도만 가능하지. 클라이언트 IP 주소로 식별하려는 시도도 있는데, IP는 사용자가 아니라 컴퓨터를 식별하는 거고, ISP가 동적 IP를 할당하고, NAT 뒤에 여러 사용자가 같은 IP로 보이고, 프락시를 거치면 프락시 IP만 보이니까 — 결론적으로 IP만으로는 불가능해.

가장 확실한 식별 방법은 로그인이야. 서버가 401 Login Required를 보내면 브라우저가 로그인 대화상자를 띄우고, 사용자가 인증하면 이후 같은 사이트 요청에 Authorization 헤더를 자동으로 포함시키지. 확실하지만 매 사이트마다 로그인하는 건 번거롭잖아.

**뚱뚱한 URL(fat URL)**은 사용자 상태 정보를 URL 자체에 포함시키는 방법이야. http://www.example.com/catalog?user_id=abc123 이런 식으로. 근데 URL이 길고 못생겨지고, 이 URL을 다른 사람에게 보내면 그 사람이 내 세션으로 접속하게 되고, 사용자마다 URL이 달라서 캐시도 안 되고, 이탈하면 세션이 끊기고 — 심각한 문제들 때문에 독자적 식별 방법으로는 거의 안 쓰여.

결국 현재 가장 널리 쓰이는 식별 기술은 쿠키야. 동작 방식은 이래: 사용자가 처음 사이트를 방문하면 서버가 Set-Cookie: id=abc123 헤더를 응답에 넣어. 브라우저가 이 쿠키를 저장하고, 이후 같은 사이트에 요청할 때마다 Cookie: id=abc123 헤더에 담아 보내지. 서버가 이걸 보고 사용자를 식별하는 거야.

쿠키에는 세션 쿠키(브라우저 닫으면 사라지는 임시 쿠키)와 지속 쿠키(디스크에 저장되어 남아있는 쿠키)가 있어. ExpiresMax-Age 파라미터가 없으면 세션 쿠키, 있으면 지속 쿠키야. 브라우저는 모든 쿠키를 모든 사이트에 보내지 않고, DomainPath 속성을 보고 해당 사이트에 맞는 쿠키만 보내. Set-Cookie: user=kim; domain=.example.com; path=/ordersexample.com/orders 경로에만 전송되는 거지. Secure 플래그는 HTTPS에서만 전송, HttpOnly는 JavaScript에서 접근 불가(XSS 공격 방어)야. 캐시와 쿠키의 상호작용도 주의가 필요한데, 한 사용자에게 맞춤 설정된 응답을 다른 사용자에게 캐시에서 제공하면 안 되니까 적절한 Cache-Control 설정이 필수야.

쿠키 자체는 보안 위협이 아니야 — 실행 가능한 프로그램을 포함하지 않거든. 하지만 **제3자 쿠키(third-party cookie)**가 문제지. 광고 네트워크가 여러 사이트에 걸쳐 쿠키를 심어서 사용자의 브라우징 습관을 추적하는 건데, 이에 대한 규제와 브라우저의 차단 정책이 점점 강화되고 있어.

이제 인증으로 넘어가자. **인증(authentication)**이란 당신이 누구인지 증명하는 거야. HTTP는 인증요구/응답(challenge/response) 프레임워크를 제공하는데, 흐름은 이래: 클라이언트가 보호된 리소스를 요청하면 → 서버가 401 UnauthorizedWWW-Authenticate 헤더로 인증 방법을 알려주고 → 클라이언트가 Authorization 헤더에 자격증명을 담아 재요청하고 → 서버가 확인 후 200 OK를 보내거나 다시 401을 보내지. 프락시 인증도 같은 구조인데 407, Proxy-Authenticate, Proxy-Authorization 헤더를 써. 서버는 보호된 리소스를 **보안 영역(realm)**으로 그룹화해서 영역마다 다른 권한 집합을 둘 수 있어.

**기본 인증(Basic Authentication)**은 가장 단순한 방식이야. 사용자 이름과 비밀번호를 콜론으로 합치고(user:password) Base64로 인코딩해서 보내는 건데, Base64는 인코딩이지 암호화가 아니라서 누구나 쉽게 디코딩할 수 있어. 네트워크를 도청하면 비밀번호를 바로 알아낼 수 있다는 거지. 재전송 공격(가로챈 자격증명을 그대로 보내서 인증 통과), 비밀번호 재사용 문제(한 사이트에서 도청당하면 다른 사이트도 위험), 메시지 변조 감지 불가, 피싱에 취약 — 이런 결함 때문에 기본 인증은 반드시 HTTPS와 함께 사용해야 해.

**다이제스트 인증(Digest Authentication)**은 기본 인증의 가장 큰 문제인 비밀번호 평문 전송을 해결하기 위해 만들어졌어. 핵심 아이디어는 비밀번호를 절대 네트워크에 보내지 않는다는 거야. 서버가 **넌스(nonce)**라는 일회용 랜덤 값을 보내면, 클라이언트가 비밀번호와 넌스를 조합해서 단방향 해시 함수로 **요약(digest)**을 계산해서 보내는 거지. 서버도 자기가 알고 있는 비밀번호와 넌스로 같은 계산을 해서 결과를 비교해.

인증요구:
WWW-Authenticate: Digest realm="admin", nonce="dcd98b...", qop="auth"

인증 응답:
Authorization: Digest username="kim", realm="admin", nonce="dcd98b...",
  uri="/admin", response="6629fae..."

넌스가 매번 바뀌니까 같은 비밀번호라도 매번 다른 요약이 생성돼. 공격자가 요약을 가로채도 재사용할 수 없는 거야 — 이게 재전송 공격 방지의 핵심이지. 요약 계산에는 A1(사용자 이름, 영역, 비밀번호 조합)과 A2(메서드, 요청 URI 조합)가 쓰이고, response = KD(H(A1), nonce:nc:cnonce:qop:H(A2)) 형태로 계산돼. qop(quality of protection) 파라미터는 auth면 인증만, auth-int면 인증에 메시지 무결성까지 포함해서 변조를 감지할 수 있어.

서버는 넌스에 타임스탬프를 포함시켜서 일정 시간이 지나면 만료시키고, stale=true로 "넌스가 만료됐으니 새 넌스로 다시 인증해"라고 알려줘 — 이때는 사용자에게 비밀번호를 다시 물어볼 필요 없이 새 넌스로 재계산만 하면 돼. 매 요청마다 401 → 재요청을 거치면 왕복이 두 배가 되니까, 사전 인증으로 서버가 Authentication-Info 헤더로 다음에 쓸 넌스를 미리 보내줘서 첫 요청부터 인증 헤더를 포함시킬 수 있게 하기도 하지.

다이제스트 인증이 기본 인증보다 훨씬 안전하지만 완벽하지는 않아. 기본 해시 알고리즘인 MD5는 이미 취약점이 알려져 있고, 중간자가 인증요구를 가로채서 더 약한 인증 방식으로 바꿔치기하는 공격도 가능하고, 서버에 저장된 비밀번호 해시가 유출되면 인증을 통과하는 데 쓸 수 있어. 현실적으로 다이제스트 인증보다는 HTTPS + 기본 인증 또는 OAuth 같은 토큰 기반 인증이 더 널리 쓰이는데, SSL/TLS가 이미 강력한 암호화를 제공하니까 그 위에서 기본 인증을 쓰는 게 더 단순하고 효과적이거든.

그래서 결국 HTTPS로 오게 돼. HTTP 자체는 평문 프로토콜이라서 도청(메시지를 엿볼 수 있음), 변조(전송 중 내용이 바뀔 수 있음), 위장(가짜 서버가 진짜인 척 할 수 있음)에 취약해. 안전한 HTTP가 제공해야 할 것은 서버 인증, 클라이언트 인증, 무결성, 암호화, 효율, 접근성 — 이걸 모두 제공하는 게 HTTPS야.

암호학의 기본부터 보면, 대칭키 암호법은 인코딩과 디코딩에 같은 키를 쓰는 방식이야(DES, 3DES, AES). 빠르지만 키 배송 문제가 있지 — 인터넷에서 처음 만나는 상대와 같은 키를 어떻게 안전하게 공유할 거냐는 거야. 공개키(비대칭키) 암호법은 키를 두 개 만들어서 하나는 공개하고(공개키) 하나는 자기만 갖는(개인키) 방식이야. 공개키로 암호화한 건 개인키로만 복호화할 수 있고, 공개키는 아무나 알아도 되니까 키 배송 문제가 사라져. 대표적인 알고리즘이 RSA지. 단점은 대칭키보다 훨씬 느리다는 건데, 그래서 실무에서는 공개키로 대칭키를 안전하게 교환하고, 이후 통신은 빠른 대칭키로 하는 방식을 써. 이게 SSL/TLS의 핵심 아이디어야.

디지털 서명은 메시지의 해시를 발신자의 개인키로 암호화한 거야. 수신자가 공개키로 서명을 복호화해서 해시를 얻고, 직접 계산한 해시와 비교하면 — 일치하면 메시지가 변조되지 않았고 발신자가 개인키를 가진 사람임이 확인돼. 무결성부인 방지를 제공하지.

공개키를 받았을 때 "이 공개키가 정말 내가 통신하려는 서버의 것인가?"를 확신하기 위해 디지털 인증서가 존재해. **인증기관(CA, Certificate Authority)**이 서버의 공개키와 도메인 정보를 확인한 후, CA의 개인키로 서명해서 인증서를 발급하지. 클라이언트가 이 인증서를 받으면 CA의 공개키로 서명을 검증해 — 브라우저에는 주요 CA들의 루트 인증서가 미리 내장되어 있어서 이게 신뢰의 시작점이 되는 거야. 실무에서는 루트 CA → 중간 CA → 서버 인증서 형태의 인증서 체인을 따라 올라가며 검증하지.

HTTPS는 HTTP over SSL/TLS야. https:// URL을 사용하고 기본 포트는 443이지. HTTP 메시지를 TCP에 직접 보내는 대신 SSL/TLS 계층을 거쳐 암호화한 후 TCP로 보내는 거야.

HTTP        (애플리케이션)
SSL/TLS     (보안)
TCP         (전송)
IP          (네트워크)

SSL 핸드셰이크 과정은 이래: 클라이언트가 지원하는 암호 알고리즘 목록과 랜덤 값을 보내면 → 서버가 알고리즘을 선택하고 인증서와 랜덤 값을 보내고 → 클라이언트가 CA 서명을 확인해서 서버를 검증하고 → 클라이언트가 프리마스터 시크릿을 서버의 공개키로 암호화해서 보내면 양쪽이 세션 키(대칭키)를 생성하고 → 이후 모든 HTTP 메시지를 세션 키로 암호화해서 전송하지. 인증서 검증 시에는 유효 기간, CA 신뢰성, 서명 유효성, 도메인 일치 여부를 확인하고, 하나라도 실패하면 브라우저가 경고를 보여줘.

하나의 서버에서 여러 도메인을 호스팅할 때 인증서 문제가 생기는데 — SSL 핸드셰이크가 HTTP 요청 전에 일어나니까 서버가 어떤 도메인인지 모르는 상태에서 인증서를 보내야 하거든. **SNI(Server Name Indication)**라는 TLS 확장이 이걸 해결해 — 클라이언트가 핸드셰이크 시작 시 접속하려는 도메인 이름을 알려주는 거야.


정리

3장 읽고 기억할 거 세 가지:

  1. 쿠키가 현실적으로 가장 널리 쓰이는 사용자 식별 방법이야. Set-Cookie로 심고 Cookie로 돌려보내는 구조이고, Domain/Path/Secure/HttpOnly 속성으로 범위와 보안을 제어하지. 제3자 쿠키를 통한 추적이 주요 개인정보 이슈야
  2. 기본 인증의 Base64는 인코딩이지 암호화가 아니야. 반드시 HTTPS와 함께 써야 하고, 다이제스트 인증은 넌스+해시로 비밀번호를 안 보내지만 MD5 취약점 때문에 현실에서는 HTTPS + 기본 인증이 더 많이 쓰여
  3. HTTPS = HTTP + SSL/TLS이고, 공개키로 대칭키를 교환한 뒤 대칭키로 통신하는 거야. CA가 서명한 디지털 인증서로 서버 신원을 보장하고, SNI로 가상 호스팅 문제도 해결하지