- 근본적인 입력 변수의 차이: HOTP는 단조 증가하는 이벤트 카운터 C(Monotonically Increasing Counter)를 해시 입력으로 사용하는 반면, TOTP는 카운터 C 대신 Unix 에포크 타임스텝 T (현재시간(초) / 30)를 사용합니다.
- 직접적인 상속 관계: 수학적으로 TOTP는 HOTP의 특수화 모델입니다:
TOTP(K, Time) = HOTP(K, T). 두 알고리즘은 완전히 동일한 동적 잘라내기(Dynamic Truncation) 함수를 공유합니다. - 아키텍처 관점의 결정적 차이: HOTP는 상태 저장형(Stateful)입니다. 서버는 매 로그인마다 카운터 값을 중앙 데이터베이스에 잠금(Lock) 및 쓰기(Write)해야 합니다. 반면 TOTP는 무상태형(Stateless)이므로, 전 세계 분산 클라우드 서버가 중앙 DB 쓰기 없이도 독립적으로 인증 코드를 검증할 수 있습니다.
역사적 배경: 독점 하드웨어 토큰에서 개방형 웹 표준으로
2000년대 초반, 기업 환경의 2단계 인증 시장은 RSA SecurID와 같은 독점 하드웨어 키팝(Key Fob) 장치가 독점하고 있었습니다. 이 폐쇄적이고 값비싼 기기들은 내부에 초정밀 수정 발진기(Quartz Crystal)를 내장하고 엔터프라이즈 전용 서버와 동기화되었습니다. 수천 명의 임직원에게 다중 인증을 보급하려면 막대한 하드웨어 구매 비용과 고가의 독점 라이선스 비용을 감당해야 했습니다.
이러한 특정 벤더 종속(Vendor Lock-in) 문제를 해결하고 인터넷 전체의 보안 표준을 확립하기 위해 업계 리더들이 결성한 연합체가 바로 OATH (Initiative for Open Authentication)입니다. OATH는 모든 개발자가 라이선스 비용 없이 자유롭게 검토하고 구현할 수 있는 IETF 개방형 표준을 제정했습니다:
HMAC-Based One-Time Password Algorithm. 물리 버튼이 달린 초소형 저전력 마이크로컨트롤러를 위해 설계되었습니다. 버튼을 누를 때마다 내부 카운터가 1 증가하고 LCD 화면에 6자리 토큰이 표시됩니다.
Time-Based One-Time Password Algorithm. 스마트폰 보급이 본격화되면서 등장했습니다. 이동통신망과 NTP 네트워크 시간 동기화를 활용하여 물리 버튼 카운터의 비동기화 문제를 원천적으로 해결했습니다.
HOTP (RFC 4226) 아키텍처 및 동적 잘라내기 바이트 메커니즘
HOTP의 핵심 수학적 정의는 직관적이면서도 암호학적으로 강력합니다:
주요 파라미터 구성:
K: 클라이언트와 서버가 공유하는 대칭 비밀키 (통상 Base32로 인코딩된 160비트 / 20바이트 바이너리 시퀀스).C: 8바이트 (64비트) 빅엔디언(Big-Endian) 정수 카운터.HMAC-SHA-1: 키 기반 해시 메시지 인증 코드로, 20바이트(160비트)의 암호학적 다이제스트를 생성합니다.
🔍 동적 잘라내기(Dynamic Truncation, DT): 20바이트 다이제스트를 6자리 숫자로 변환하는 과정
HMAC-SHA1의 결과물은 20바이트(160비트) 바이너리 데이터입니다. 이를 인간이 입력하기 쉬운 균일한 6자리 10진수로 변환하기 위해 RFC 4226 Section 5.3에 명시된 결정론적 바이트 추출 메커니즘을 적용합니다:
0x0F로 비트 AND 마스킹을 수행합니다. 그 결과는 정확히 0부터 15 사이의 정수가 됩니다. 15에 4바이트를 더해도 19이므로, 이후 연속 4바이트 추출 시 버퍼 오버런이 원천 차단됩니다.
0x7F로 마스킹하여 부호 비트(Sign bit)를 제거함으로써, 부호 있는 정수(Signed Integer) 처리 과정에서 발생할 수 있는 CPU 아키텍처별 오버플로를 방지하고 순수 양수 31비트 정수를 얻습니다.
비밀키가 ASCII 문자열 "12345678901234567890"이고 카운터가 C = 0일 때, HMAC-SHA1의 16진수 다이제스트는 다음과 같습니다:
1. 마지막 바이트는 0x71입니다. 0x71 & 0x0F = 0x01이므로 Offset = 1이 됩니다.
2. 1번 바이트부터 4개 바이트는 93 cf 18 50입니다. 최상위 비트를 제거하면 0x13cf1850이 되며, 이는 10진수로 332,339,280입니다.
3. 332339280 mod 1000000 = 339280입니다. 따라서 최종 도출되는 6자리 OTP는 정확히 339280입니다!
TOTP (RFC 6238)의 진화: 시간 단계(Time-Step)의 마법
HOTP는 클라이언트의 카운터와 서버의 카운터가 엄격히 일치해야만 동작합니다. 하지만 소프트웨어 앱 환경에서 사용자의 버튼 클릭 횟수를 신뢰성 있게 유지하는 것은 사람의 실수로 인해 매우 취약합니다.
RFC 6238은 카운터 C 대신 Unix 에포크 시계(Epoch Time)에서 직접 파생되는 이산 시간 단계(Time-Step) T를 도입했습니다:
- CurrentUnixTime: 1970년 1월 1일 00:00:00 UTC 이후 경과한 현재 시간(초).
- T0: 시간 기준점 (모든 최신 상용 구현에서는 0으로 고정).
- X (시간 스텝 윈도우): 표준 권장 유효 시간 간격인 30초.
- 바닥 함수 ⌊ ⌋ (Floor Function): 30초 구간 내에서는 T 값이 동일하게 유지됩니다. 예를 들어 에포크 초 1728211200부터 1728211229까지 30초 동안 T는 정확히 동일한 정수로 계산됩니다.
바이너리 직렬화: 계산된 정수 T는 HOTP의 카운터 C와 완전히 동일한 메모리 레이아웃인 8바이트 빅엔디언 바이너리 버퍼로 변환됩니다. 따라서 단 한 줄의 아름다운 통합 수식으로 요약됩니다:
TOTP(K, Time) = HOTP(K, T)
이 수학적 간결성 덕분에 전 세계 모든 인증기 앱은 네트워크 통신 없이도 30초마다 서버와 완벽히 일치하는 6자리 코드를 롤링할 수 있습니다.
HS[31] & 0x0F)에서 오프셋을 추출하며, SHA-512(64바이트 다이제스트)에서는 63번 바이트(HS[63] & 0x0F)에서 추출합니다. 그러나 구글, 마이크로소프트, 깃허브 등 전 세계 99.9% 서비스에서는 기존 클라이언트 앱 호환성을 위해 여전히 SHA-1을 기본값으로 사용하고 있습니다.
HOTP vs TOTP 종합 비교 매트릭스
| 비교 차원 | HOTP (RFC 4226) | TOTP (RFC 6238) |
|---|---|---|
| 입력 변수 (Input Variable) | 이벤트 카운터 C (단조 증가 정수) | 시간 단계 T (30초 Unix 에포크 윈도우) |
| 토큰 유효 수명 (Lifespan) | 영구적 (다음 로그인에 사용될 때까지 유효) | 엄격한 30초 (시간 만료 시 자동 무효화) |
| 서버 아키텍처 (Server Architecture) | 상태 저장형 (로그인마다 DB 카운터 원자적 쓰기 필요) | 무상태형 (NTP 시계 동기화 기반 독립 검증) |
| 재생 공격(Replay Attack) 노출도 | 중간 (탈취된 코드는 사용 전까지 유효) | 매우 낮음 (해당 30초 잔여 시간으로 제한) |
| 주요 장애 모드 (Primary Failure) | 카운터 비동기화 (토큰 버튼 누적 오작동) | 시간 드리프트 (스마트폰 기기 시계 불일치) |
| 지원 해시 알고리즘 | HMAC-SHA-1 단독 | HMAC-SHA-1, HMAC-SHA-256, HMAC-SHA-512 |
| 실제 산업계 채택률 | 레거시 은행 OTP 토큰, 일부 오프라인 출입 보안 | 현대 웹·모바일 2FA의 99.9% |
카운터 비동기화 vs 시간 드리프트: 시스템적 해결책
실제 프로덕션 환경에서는 클라이언트와 서버의 상태가 필연적으로 어긋날 수 있습니다. 암호학자들은 서비스 안정성을 위해 정교한 오차 보정 메커니즘을 설계했습니다:
사용자가 주머니 속에서 하드웨어 버튼을 5번 눌렀다면, 클라이언트 카운터는 C+5가 되지만 서버는 여전히 C에 머물러 있습니다.
RFC 4226 규격에 따라 서버는 인증 시 현재 카운터부터 앞선 구간 [C+1, C+s](통상 s=20)까지 코드를 미리 계산해 일치 여부를 탐색합니다. 일치하는 코드가 발견되면 서버는 카운터를 즉시 해당 위치로 전진 동기화합니다.
네트워크 전송 지연이나 사용자의 스마트폰 시계가 몇 초 정도 느린 경우, 코드가 다음 시간 윈도우에 서버에 도착할 수 있습니다.
엔터프라이즈 서버는 현재 스텝 T뿐만 아니라 이전 스텝(T-1)과 다음 스텝(T+1)을 포함하는 총 3개 윈도우(총 90초 범위)를 허용 범위로 검증합니다.
서버가 사용자가 제출한 6자리 코드와 자신이 계산한 코드를 비교할 때, 일반 문자열 비교 연산자(userOtp === serverOtp)를 사용하면 첫 글자가 틀린 순간 즉시 연산이 종료됩니다. 공격자는 미세한 네트워크 왕복 응답 시간(나노초 단위)을 측정하여 올바른 숫자를 한 자리씩 추측할 수 있습니다. 백엔드 시스템은 반드시 상수 시간 비교 함수(예: Node.js의 crypto.timingSafeEqual 또는 Go의 subtle.ConstantTimeCompare)를 적용해야 합니다.
TOTP가 현대 인터넷 인증을 지배하게 된 3대 이유
웹 생태계 전체가 HOTP 대신 TOTP를 표준 다중 인증 수단으로 전폭 채택한 데에는 세 가지 핵심 기술적 이유가 있습니다:
전 세계에 분산된 마이크로서비스 환경에서, NTP로 동기화된 모든 서버 인스턴스는 중앙 데이터베이스 조회나 쓰기 락 없이 로컬 메모리에서 즉각 TOTP를 검증할 수 있습니다.
스마트폰은 기지국과 GPS를 통해 시스템 시계를 항상 1초 미만의 오차로 자동 보정하므로 하드웨어 유지보수 비용이 전혀 들지 않습니다.
HOTP 토큰은 사용될 때까지 영구 유효하지만, TOTP 토큰은 30초 후 소멸하므로 유출된 패킷이나 어깨너머 엿보기(Shoulder Surfing) 공격을 자동 무력화합니다.
2FA-Tool에서 실시간 TOTP 연산을 직접 확인해보세요
Base32 시드 문자열이 HMAC-SHA1 동적 잘라내기를 통해 어떻게 실시간 6자리 인증 코드로 변환되는지 100% 오프라인 브라우저 샌드박스에서 검증하세요.
07. 자주 묻는 질문 (FAQ)
Q1: HOTP가 카운터 비동기화(Desynchronization)에 취약한 근본적인 이유는 무엇인가요? ▼
HOTP는 토큰 장치의 물리 버튼을 누를 때마다 내부 카운터 C를 1씩 증가시킵니다. 사용자가 실수로 주머니 속에서 버튼을 20번 눌렀지만 해당 코드를 서버에 제출하지 않은 경우, 토큰의 카운터는 C+20으로 전진하지만 서버의 데이터베이스는 여전히 C에 머물러 있게 됩니다. 이 격차가 서버의 사전 탐색 검증 윈도우(s)를 초과하면 수동 재동기화 프로토콜을 수행하기 전까지 모든 인증 코드가 영구적으로 거부됩니다.
Q2: SHA-1 해시 충돌 취약점이 알려져 있는데, TOTP에서 SHA-1을 사용하는 것은 위험하지 않나요? ▼
전혀 위험하지 않습니다. TOTP는 단순 SHA-1 해시가 아닌 HMAC-SHA-1(Keyed-Hash Message Authentication Code) 구조를 사용합니다. HMAC의 암호학적 보안성은 해시의 의사난수 함수(PRF) 속성에 기반하며, 단순한 충돌 공격이 비밀키 복구나 메시지 위조 공격으로 이어지지 않습니다. 또한 생성된 코드는 30초 후 만료되므로 오프라인 무차별 대입 공격(Brute-Force)이 계산적·경제적으로 불가능합니다.
Q3: 왜 TOTP는 서버 측에서 무상태(Stateless) 검증이 가능한가요? ▼
HOTP 환경에서는 로그인 성공 시마다 서버가 중앙 집중식 데이터베이스에 카운터 C의 최신 값을 원자적으로 기록(Write)해야 하므로 대규모 분산 클러스터에서 쓰기 잠금 병목 현상이 발생합니다. 반면 TOTP는 시간 스텝 T를 NTP로 동기화된 시스템 시계에서 직접 도출하므로, 로드 밸런싱된 전 세계 분산 서버 노드가 중앙 DB 쓰기 작업 없이 독립적으로 인증 토큰을 검증할 수 있습니다.
Q4: 스마트폰의 시계가 표준 시간과 10초 정도 어긋나 있어도 TOTP 인증이 가능한가요? ▼
네, 가능합니다. 프로덕션 환경의 TOTP 검증 서버는 네트워크 지연과 단말기 시계 오차를 감안하여 현재 시간 스텝 T뿐만 아니라 이전 스텝(T-1)과 다음 스텝(T+1)을 모두 허용하는 3구간 오차 범위(총 90초 유효)를 적용합니다. 따라서 10초 정도의 미세한 시간 차이는 정상적으로 통과됩니다. 단, 30초 이상 차이가 날 경우 인증기 앱 내의 '시계 동기화' 기능을 실행해야 합니다.