인증의 과정을 사람이 말하듯이

인증의 과정을 사람이 말하듯이

수많은 웹사이트가 있다. 그런데 이 사이트가 진짜 내가 아는 그 사이트인지 확인하고 싶다. 그리고 확인이 끝나면, 남이 엿듣지 못하게 둘만 아는 말로 대화하고 싶다. 이 두 가지를 어떻게 해내는지, 부품을 하나씩 꺼내 조립해 본다.

나만 가지고 있는 값 (보내지 않음) 인터넷에서 보이는 값 각자 계산해서 얻는 값 고친 점 (처음에 헷갈렸던 이해)
1 · 문제

이 사이트가 진짜인지 어떻게 알까?

CA(Certificate Authority, 인증기관)라는 곳이 있다. 브라우저와 OS가 믿기로 미리 정해 둔 기관이다.

CA는 사이트에게 인증서를 발급해 준다. 인증서는 "이 사이트는 진짜 bank.com이 맞다"를 CA가 보증하는 문서다. 사이트는 접속한 나에게 이 문서를 보여 주고, 나는 문서가 진짜면 사이트도 진짜라고 믿는다.

그런데 이 문서가 위조된 게 아닌지는 어떻게 확인할까?

이걸 풀려면 먼저 공개키와 개인키를 알아야 한다.

2 · 부품

공개키와 개인키

공개키와 개인키는 한 쌍으로 만들어진다. 누구나 만들 수 있다. 개인키는 절대 남에게 주면 안 되고, 공개키는 이름 그대로 누구에게나 나눠 준다.

둘이 하는 일을 나란히 놓고 보면 이렇다.

공개키개인키
누가 가지고 있나누구나주인만
남에게 주나나눠 준다절대 주지 않는다
잠그기(암호화)에서잠근다연다 (개인키로만 열린다)
서명에서확인만 한다만든다 (개인키로만 만들 수 있다)
못 하는 것열기, 서명 만들기없음
인감으로 치면인감증명서에 찍혀 있는 인감 모양인감도장
TLS 1.3에서은행 서명을 확인할 때은행이 서명을 만들 때

잠그기는 공개키가 먼저 움직이고, 서명은 개인키가 먼저 움직인다. 어느 쪽이든 공개키로는 잠긴 걸 열 수도 없고 서명을 만들 수도 없다. 그래서 아무에게나 줘도 된다.

고친 점

"공개키는 잠그거나(만들거나) 확인만 할 수 있다"

"만들거나"는 빼야 한다. 서명을 만드는 건 개인키만 할 수 있다. 공개키는 잠그기와 서명 확인, 두 가지만 한다. 그리고 뒤에 나오는 TLS 1.3에서는 잠그기는 쓰지 않고 서명만 쓴다.

3 · 부품

인증서는 CA가 써 준 증명 문서

인증서는 카드가 아니라 문서다. CA가 "이 공개키의 주인은 bank.com이다"라고 적고, 그 내용에 자기 도장을 찍어 준 문서. 우리 생활에서 가장 가까운 건 인감증명서다.

인감증명서인증서
발급하는 곳주민센터CA
누구 것인지이름, 주소도메인(bank.com), 유효기간
대조용 견본찍혀 있는 인감 모양은행 공개키
발급기관 도장주민센터 직인CA 서명
본인만 가진 것 (문서에는 없음)인감도장은행 개인키

문서에 키가 어떻게 들어 있지?

공개키는 결국 아주 긴 숫자다. 숫자니까 문서에 글자로 적을 수 있다. 실제 우리은행(www.wooribank.com) 인증서를 펼쳐 보면 이렇게 적혀 있다.

인 증 서
발급 대상
www.wooribank.com
Woori Bank Co., Ltd.
발급 기관
GeoTrust EV RSA CA G2 (DigiCert Inc)
유효 기간
2026-07-21 ~ 2027-02-05
공개키
RSA 2048비트
모듈러스 AC 63 E6 B6 45 38 6E 57 AF 54 AF CE A0 F6 1A A7 …
지수 (보통 65537)
CA 서명"인증서 서명 값" 칸. GeoTrust EV RSA CA G2가 자기 개인키로 위 내용 전체에 만든 숫자

2026년 10월에 브라우저 인증서 뷰어로 확인한 값이다. 이 인증서는 2027년 2월에 만료되고 새 것으로 바뀐다. 주소창 왼쪽 아이콘을 누르면 지금 보는 사이트의 인증서를 직접 열어 볼 수 있다. 공개키는 "세부정보 → 대상 공개 키 정보 → 대상의 공개 키", CA 서명은 "세부정보 → 인증서 서명 값"에 있다.

RSA 공개키는 모듈러스와 지수, 두 숫자 한 쌍이다. 모듈러스 2048비트는 10진수로 617자리 정수다. 아주 큰 소수 두 개를 곱한 값이라, 그 두 소수를 아는 우리은행만 개인키를 가질 수 있다. 617자리 숫자를 다시 두 소수로 쪼개는 건 사실상 불가능하다. 이름에 RSA가 들어 있지만, TLS 1.3에서 이 키는 데이터를 잠그는 데 쓰지 않고 서명에만 쓴다.

확인하는 법은 인감증명서와 같다. 주민센터 직인이 진짜인지 보듯, CA 공개키로 CA 서명이 진짜인지 확인한다. 서명이 맞으면 "이 공개키는 www.wooribank.com 것"이라는 내용을 믿을 수 있다.

인증서 뷰어의 SHA-256 지문은 무엇일까?

인증서 뷰어 첫 화면 아래쪽에는 "SHA-256 지문"이 두 줄 나온다. 서명처럼 보이지만 아니다. 브라우저가 보여 주려고 계산한 해시값이다.

뷰어 항목우리은행 값정체
인증서af6bccaeea1be16b…인증서 파일 전체의 해시. 인증서끼리 구별하는 용도
공개 키6e87ff9fb6eff8d5…우리은행 공개키의 해시. 공개키 본체가 아니다
고친 점

"지문의 '공개 키'가 우리은행 공개키이고, '인증서'가 우리은행의 서명이다"

둘 다 해시값이다. 긴 값을 64자리로 줄여서 비교하기 쉽게 만든 것뿐이다. 인증서 지문은 인증서 안에 들어 있을 수도 없다. 자기 해시를 자기 안에 넣으면 해시가 또 바뀌기 때문이다. 그리고 우리은행의 서명은 인증서 어디에도 없다. 접속할 때마다 그 자리에서 새로 만드는 값이라서, 인감증명서에 계약서 도장이 미리 찍혀 있지 않은 것과 같다.

도장은 두 단계로 찍혀 있다

우리은행 인증서의 발급 기관 GeoTrust EV RSA CA G2는 브라우저에 내장된 CA가 아니라 중간 CA다. 세부정보 탭의 "인증서 계층"을 보면 세 단계가 위아래로 나온다.

  1. DigiCert Global Root G2. 브라우저와 OS에 내장된 루트 CA다. 이 공개키로 아래 중간 CA 인증서의 서명을 확인한다.
  2. GeoTrust EV RSA CA G2. 중간 CA다. 확인된 이 공개키로 아래 우리은행 인증서의 서명을 확인한다.
  3. www.wooribank.com. 확인된 우리은행 공개키로, 접속할 때 은행이 그 자리에서 찍은 서명을 확인한다.

주민센터 직인을 시청이 보증해 주는 식이다. 은행은 접속할 때 자기 인증서와 중간 CA 인증서를 같이 보내고, 브라우저는 내장된 루트부터 차례로 확인하며 내려온다. 참고로 EV는 CA가 도메인뿐 아니라 회사 실체까지 확인하고 발급한 인증서라는 뜻이다. 그래서 발급 대상에 Woori Bank Co., Ltd.라는 회사 이름까지 적혀 있다.

고친 점

"이름 + 유효기간 + CA의 서명이 있는 인증서를 CA 개인키로 잠가서 배포한다"

인증서는 잠그지 않는다. 누구나 읽을 수 있게 적어 두고, 그 내용에 CA가 서명만 한다. 잠그면 오히려 아무도 못 읽는다. 그리고 빠진 내용이 하나 있다. 인증서에서 제일 중요한 사이트의 공개키가 들어 있다. 이 공개키는 8장에서 은행 서명을 확인할 때 쓴다.

인증서가 진짜인 건 알았다. 그런데 남의 인증서를 복사해 온 가짜라면?

인감증명서도 복사본을 들고 다닐 수 있다. 그래서 계약할 때는 그 자리에서 인감도장을 찍게 하고, 증명서에 찍힌 인감 모양과 대조한다. TLS도 똑같다. 은행이 그 자리에서 개인키로 서명을 하고, 나는 인증서에 적힌 공개키로 대조한다. 인증서는 누구나 복사할 수 있지만 개인키는 진짜 은행만 가지고 있다. 이 과정은 8장에서 본다.

4 · 부품

해시, 한 방향으로만 가는 계산

확인하는 방법은 알았으니, 이제 보내는 방법을 고민해 보자. 그 전에 해시라는 부품이 하나 더 필요하다. SHA-256을 가장 많이 쓴다.

해시는 입력을 넣으면 고정 길이 값이 나오는 한 방향 계산이다. 같은 입력은 항상 같은 값이 나오고, 결과에서 입력을 되찾을 수 없다. 입력이 한 글자만 달라도 결과는 완전히 달라진다.

SHA-256 직접 계산해 보기

한 글자만 바꿔 보면 결과 전체가 바뀐다. 결과 길이는 입력 길이와 상관없이 항상 64자리(256비트)다.

그래서 사이트는 비밀번호를 저장할 때 원문이 아니라 해시를 저장한다. 로그인할 때 들어온 비밀번호를 같은 방식으로 해시해서, 저장해 둔 값과 같으면 통과시킨다. 서버 DB가 털려도 원문은 바로 나오지 않는다.

고친 점

"830915를 입력하면 FD0DAEFD0ED7204F…"

830915의 SHA-256은 D97B47C951B806DE…다. 위 상자에서 확인할 수 있다. FD0D…는 다른 입력이거나 다른 계산 방식에서 나온 값으로 보인다.

고친 점

"해시 때문에 괜찮다. 결과로 입력을 알 수 없다. 이론상만 가능하다고 하는데 그냥 없다."

거꾸로 계산하는 방법은 없는 게 맞다. 하지만 후보를 전부 넣어 보는 방법은 있다. 6자리 숫자는 후보가 100만 개뿐이라, 하나씩 해시해서 같은 값이 나올 때까지 돌리면 된다. 아래에서 직접 해 보자.

해시만 보고 6자리 비밀번호 찾아보기

공격자가 손에 넣은 것은 이 해시값뿐이다.

그래서 실제 사이트는 해시를 그냥 쓰지 않고 두 가지를 더한다.

그래도 비밀번호를 여러 사이트에 같이 쓰는 건 여전히 위험하다. 모든 사이트가 제대로 해시한다는 보장이 없고, 피싱으로 원문을 직접 입력받으면 해시는 아무 소용이 없다.

5 · 부품

MAC과 HMAC, 비밀 키를 섞은 해시

해시를 설명한 이유는 HMAC을 말하기 위해서다. 그 전에 MAC부터 보자.

MAC (Message Authentication Code)

메시지를 해시 함수에 통과시키기 전 원본을 메시지, 통과한 결과를 다이제스트(digest)라고 부른다. 여기에 보내는 쪽과 받는 쪽만 아는 비밀 키를 섞어서 만든 작은 값이 MAC이다.

  1. 보내는 쪽: 메시지 + 비밀 키 → MAC 계산 → 메시지와 MAC을 같이 보냄
  2. 받는 쪽: 받은 메시지 + 자기가 가진 같은 비밀 키 → MAC을 다시 계산
  3. 받은 MAC과 같으면: 메시지가 안 바뀌었고(무결성), 비밀 키를 가진 그 상대가 보냈다(메시지 인증)
고친 점

"수신자는 송신자가 사용한 동일한 해시함수로 결과값을 비교해서 변조를 조사한다"

해시 함수만 같으면 안 되고 같은 비밀 키가 있어야 한다. 비밀 키 없이 해시만 쓰면, 중간에서 메시지를 바꾼 사람도 해시를 새로 계산해서 같이 바꿔 보내면 그만이다. 비밀 키가 들어가야 "비밀 키 가진 사람만 맞는 값을 만들 수 있다"가 성립한다.

MAC에는 두 가지 성질이 있다.

HMAC (Hash-based MAC)

HMAC은 MAC을 해시 함수로 만드는 표준 방법이다. 비밀 키와 메시지를 정해진 방식으로 섞어 해시를 두 번 한다. 같은 비밀 키를 가진 쪽만 같은 결과를 만들 수 있다.

일반 해시HMAC
증명하는 것무결성만 (그것도 우연한 손상까지만)무결성 + 메시지 인증
중간에서 바꾸면해시도 새로 계산해 바꿔 보내면 못 알아챔비밀 키가 없어서 맞는 값을 못 만듦
길이 확장 공격해시(키 + 메시지)처럼 직접 섞으면 취약해시를 두 번 하는 구조라 안전

HMAC-SHA256 직접 계산해 보기 (아래 Python 코드와 같은 계산)

보내는 쪽이 만든 HMAC (메시지와 함께 전송)
받는 쪽이 다시 계산한 HMAC

받은 메시지를 한 글자 바꾸거나, 받는 쪽 비밀 키를 바꿔 보자.

Python으로 만든 같은 검증 코드
import hmac
import hashlib

def create_hmac(key, message):
    if isinstance(key, str):
        key = key.encode('utf-8')
    if isinstance(message, str):
        message = message.encode('utf-8')
    return hmac.new(key, message, hashlib.sha256).hexdigest()

def verify_hmac(key, message, received_hmac):
    calculated = create_hmac(key, message)
    # 상수 시간 비교: 몇 글자째에서 틀렸는지 응답 시간으로 새지 않게
    return hmac.compare_digest(calculated, received_hmac)

secret_key = "my_secret_key"
message = "Hello, HMAC!"
signature = create_hmac(secret_key, message)
print(signature)
# 2135134b3d2d6ac11a8eb3f6340dac59282a4edbb21ba861772ab2cb3ca56942
print(verify_hmac(secret_key, message, signature))              # True
print(verify_hmac(secret_key, message + " 변조됨", signature))  # False

TLS 1.3에서 HMAC은 두 군데 쓰인다. 핸드셰이크 끝의 확인 메시지(Finished), 그리고 키 교환 결과에서 실제로 쓸 키들을 뽑아내는 HKDF. 연결 뒤 실제 데이터는 AES-GCM 같은 방식으로 잠그는데, 여기에는 무결성 확인이 암호화 안에 같이 들어 있어서 HMAC을 따로 계산하지 않는다.

6 · 부품

키를 보내지 않고 같은 키를 만드는 법

어떻게 비밀 키를 보내지 않고 너와 내가 같은 키를 만들고, 그게 같다는 걸 어떻게 알까?

이 질문에 답하려면 키 교환을 알아야 한다. 먼저 왜 필요한지부터.

방법문제
대칭키를 미리 나눠 갖기잠그는 키와 여는 키가 같아서 빠르다. 하지만 처음 접속하는 사이트와는 미리 나눌 방법이 없다.
모든 데이터를 공개키로 잠가 주고받기공개키 계산은 대칭키보다 수백~수천 배 느리다. 그리고 내 브라우저는 자기 키 쌍이나 인증서가 없다.
대칭키를 만들어 은행 공개키로 잠가 보내기TLS 1.2까지 쓰던 RSA 방식. 누가 통신을 녹화해 뒀다가 몇 년 뒤 은행 개인키를 훔치면 과거 통신이 전부 풀린다. 그래서 TLS 1.3에서 없앴다.

그래서 가장 좋은 건 서로 같은 대칭키를 갖는 것. 지금은 없으니, 이번 연결에만 쓸 같은 키를 그 자리에서 만들자. 이게 키 교환이다.

작은 숫자로 보는 원리

공개값(누구나 앎)은 밑수 5, 나눗수 23. 계산은 "5를 n번 곱해서 23으로 나눈 나머지"다.

나 (브라우저)

6
보낼 재료 = 56 mod 23 = 8
키 = 196 mod 23 = 2

은행

15
보낼 재료 = 515 mod 23 = 19
키 = 815 mod 23 = 2

인터넷에서 보이는 건 재료 두 개뿐이다. 비밀숫자와 최종 키는 한 번도 밖으로 나가지 않는다. 실제 크기에서는 재료를 보고 비밀숫자를 되찾을 수 없다(이산 로그 문제).

실제로 쓰는 방식, X25519

X25519는 지금 가장 많이 쓰는 키 교환 방식이다. 다니엘 J. 번스타인이 제안한 Curve25519라는 타원곡선을 쓴다. 원리는 위와 같고, "거듭제곱" 대신 "곡선 위의 점을 a번 더하기"를 쓴다. 같은 안전도를 훨씬 작은 숫자로 얻을 수 있다. 숫자는 전부 정수다.

단계앨리스 (브라우저)밥 (은행)
1. 개인키 생성a (32바이트 무작위 수)b (32바이트 무작위 수)
2. 공개키 계산A = a · G (G는 곡선의 기준점)B = b · G
3. 교환A를 보냄 →← B를 보냄
4. 공동 비밀 계산K = a · B = a · (b · G)K = b · A = b · (a · G)

둘 다 K = (a · b) · G라는 같은 값을 손에 쥔다. 도청자는 A와 B를 봐도 a나 b를 알아낼 수 없어서(타원곡선 이산 로그 문제, ECDLP) K를 계산하지 못한다.

실제 크기의 값 보기 (표준 문서 RFC 7748의 예시)
나 비밀    77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a
나 재료    8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a
은행 재료  de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f
공동 비밀  4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742

16진수로 적었을 뿐 모두 77자리 안팎의 정수다. 위 6, 8, 19, 2와 같은 자리에 있는 값들이다.

고친 점

"공동 비밀이 최종 대칭키(AES 등)로 사용된다"

공동 비밀을 그대로 쓰지는 않는다. HKDF(HMAC으로 만든 키 뽑기 함수)에 넣어서 용도별 키를 여러 개 뽑아 쓴다. 확인 메시지용, 나→은행 데이터용, 은행→나 데이터용이 따로다. 하나가 어디서 새도 다른 키까지 번지지 않게 하려는 것이다. 이 페이지에서는 편의상 공동 비밀 2를 "키"라고 부른다.

7 · 조립 전에

세 부품의 역할은 다르다

여기까지 키 교환, 서명, HMAC이 나왔다. 셋이 막는 공격이 각각 다르다. 이걸 섞으면 TLS 흐름이 헷갈린다.

키 교환
엿보기를 막는다

재료만 오가니까 엿본 사람은 키를 못 만든다.

서명
끼어들기를 막는다

은행 개인키가 있어야 만들 수 있으니, 가운데 낀 가짜는 은행인 척을 못 한다.

HMAC
같은 키, 같은 대화를 확인한다

둘이 정말 같은 키를 만들었고 같은 대화를 봤는지 마지막에 맞춰 본다.

고친 점

"누군가가 우리의 메시지를 엿들을 수 있으니, HMAC로 무결성을 증명하자"

엿듣기는 키 교환이 이미 막았다. 남은 위험은 가운데 누가 끼어들어 은행인 척하는 것이고, 이건 은행의 서명이 막는다. HMAC(확인 메시지)은 그 뒤에 "우리 둘 다 같은 키를 가졌고, 주고받은 내용도 똑같이 봤다"를 마지막으로 맞춰 보는 역할이다.

가운데에 해커가 끼면 어떻게 될까

8 · 조립

TLS 1.3 한 바퀴

브라우저와 은행 사이트가 있다. 브라우저는 은행 사이트에 접속하고 싶어서 연결을 시도한다. 위 부품들이 어떤 순서로 쓰이는지 따라가 보자. 숫자는 6장의 작은 숫자를 그대로 쓴다.

브라우저가 아는 것
인터넷에서 보이는 것
은행이 아는 것
고친 점

"은행은 19와 인증서(인증서에는 은행 공개키와 은행임을 증명하는 서명, 받은 8도 있다)를 보낸다"

서명이 두 개이고 들어 있는 곳이 다르다.

  • 인증서 안: 은행 공개키 + CA의 서명. 인증서는 미리 발급받아 둔 것이라 8은 들어 있지 않다.
  • 인증서와 별도로: 은행이 지금 막 만든 은행의 서명. 이 서명이 찍힌 범위가 "지금까지의 대화 전체"이고, 거기에 8과 19가 포함된다. 8을 다시 보내는 게 아니라, 8을 포함한 내용에 서명만 한다.

그래서 확인도 두 번이다. CA 공개키로 인증서(CA 서명)를 확인해 은행 공개키를 꺼내고, 그 은행 공개키로 은행 서명을 확인한다.

고친 점

"19를 X25519에 넣어 보니 2가 나왔다" / "은행도 8을 X25519에 넣어 계산하니 2"

받은 재료만 넣는 게 아니라 받은 재료 + 내 비밀숫자를 같이 넣는다. 브라우저는 19와 6, 은행은 8과 15. 비밀숫자가 빠지면 엿본 사람도 같은 계산을 할 수 있다.

9 · 정리

한 장 요약

  1. 가장 좋은 건 서로 같은 키(대칭키)를 갖는 것. 빠르다. 하지만 처음 만난 사이트와는 미리 나눠 가질 수 없다.
  2. 그래서 이번 연결에 쓸 같은 키를 그 자리에서 만든다. 키 교환(X25519). 재료만 주고받으니 엿본 사람은 키를 못 만든다.
  3. 가운데 누가 끼어 은행인 척할 수 있으니, 은행이 진짜임을 증명한다. CA가 서명한 인증서에서 은행 공개키를 꺼내고, 그 공개키로 은행이 지금 막 찍은 서명을 확인한다. 서명 범위에 내가 보낸 재료가 들어 있으니 중간에 바꿔치기도 들킨다.
  4. 마지막으로 정말 같은 키, 같은 대화인지 맞춰 본다. 키는 보내지 않고, 키로 계산한 HMAC 값만 서로 보낸다.
  5. 이후 데이터는 그 키에서 뽑은 대칭키로 잠가서 주고받는다. 연결이 끝나면 비밀숫자는 버린다. 나중에 은행 개인키가 털려도 과거 통신은 못 푼다(전방 비밀성).

한 문장으로: 신원 확인은 은행의 고정 키(서명)로, 암호화 키는 매번 새로 각자 계산해서.

여기까지가 "브라우저가 은행을 확인하는" 단계다. 은행은 아직 내가 누군지 모른다. 그다음은 이 통로 안에서 로그인하고, 번호표(세션)나 서명된 토큰(JWT)으로 로그인 상태를 유지하는 이야기다.