[PonsWarp 개념 교실 22] TURN 자격증명 — coturn REST API와 HMAC-SHA1
쉽게 말하면, TURN 자격증명은 "잠깐만 쓸 수 있는 릴레이 이용권"이고, username 안에 만료시각을 넣고 그 전체를 HMAC-SHA1으로 서명한다.

직접 WebRTC 연결이 NAT나 방화벽에 막히면 TURN이 두 브라우저 사이에서 데이터를 릴레이한다. 대신 아무나 영원히 쓸 수 있게 두면 비용과 보안 문제가 생긴다.
username에 만료시각을 넣는다
rustlet expiry_time = now + config.credential_ttl; let base_username = format!("user_{}_{:x}", now, random); let credential_username = format!("{}:{}", base_username, expiry_time);
서버는 현재 시각과 랜덤값, 만료시각을 합쳐 username을 만든다. coturn은 이 username을 보고 언제까지 유효한지 판단한다.
password는 HMAC 결과
rustlet password = generate_hmac_hash( &credential_username, &config.secret, );
TURN shared secret은 프론트에 노출하지 않는다. 서버만 알고 있는 secret으로 username을 HMAC-SHA1 해시해 password를 만든다.
username = user_timestamp_random:expiry
password = HMAC-SHA1(TURN_SECRET, username)
클라이언트는 username/password를 TURN에 제출하고, TURN 서버는 같은 shared secret으로 계산해 일치 여부를 검증한다.
만료와 상수시간 검증
username에서 expiry를 파싱해 만료 여부를 먼저 확인하고, 자격증명 비교는 constant-time 방식으로 수행한다. 만료된 credential은 재발급해야 하며, TURN_SECRET 자체는 절대 브라우저에 보내지 않는다.
PonsWarp 적용 지점
| 항목 | 값 | 증거 |
|---|---|---|
| username | user_{timestamp}_{random}:{expiry} | turn.rs:58-63 |
| 서명 | HMAC-SHA1 | turn.rs:65-75 |
| 기본 TTL | 3600초 | config.rs:471-500 |
| ICE 광고 | STUN/TURN URL + username/credential | turn.rs:80-111 |
| 검증 | expiry + constant-time HMAC 비교 | turn.rs:155-178 |
username이 만료시각을 운반한다
TURN 자격증명은 서버가 매번 장기 비밀번호를 저장하는 방식이 아니다. username 안에 만료 시각을 넣고, coturn이 기대하는 username:password 조합을 HMAC-SHA1으로 계산한다. 서버는 같은 shared secret을 알고 있는 coturn만 검증할 수 있도록 한다.
textnow + ttl ──► expiry timestamp │ username = expiry:opaque-id │ HMAC-SHA1(shared secret, username) │ password
입력은 현재 시각, TTL, 랜덤 식별자, TURN secret이다. 출력은 username, credential, urls이며, expiry가 지난 뒤에는 같은 문자열을 재사용해도 유효하지 않다. HMAC는 username이 바뀌면 password도 바뀌게 하므로 username의 만료시각을 검증 데이터에 포함한다.
그냥 TURN 비밀번호를 오래 발급하면 안 되나?
장기 자격증명은 발급된 문자열이 유출될 때 회수 범위가 넓다. 짧은 TTL은 사용 중인 연결이 만료되는 트레이드오프가 있으므로, 연결 수립과 재연결 정책에 맞는 TTL을 정해야 한다. TTL을 지나치게 짧게 잡으면 정상적인 ICE 재시도도 실패하고, 너무 길면 릴레이 자원을 오래 점유한다.
STUN은 주소 발견을 돕고 TURN은 실제 릴레이 데이터 경로를 제공한다. 둘을 같은 것으로 보면 직접 연결 실패 시 왜 credential이 필요한지 설명할 수 없다. 또한 HMAC 계산이 맞아도 요청 시각을 서버에서 검사하지 않으면 만료가 제대로 적용되지 않는다.
| 검증 지점 | 확인할 것 |
|---|---|
| 시계 | server·coturn 시간 차이 |
| expiry | 현재 시각보다 미래인가 |
| secret | 환경변수에만 존재하는가 |
| HMAC | username 전체를 입력으로 삼는가 |
| TTL | 연결 수립·재시도 시간을 감당하는가 |
재구축할 때는 password를 로그에 남기지 않고, 실패 시 “서명 불일치”와 “만료”를 운영상 구분할 수 있을 정도의 안전한 메타데이터만 남긴다. TURN credential은 연결을 보조하는 단기 권한이지 사용자 인증 자체가 아니다.
한 문장 정리
PonsWarp는 TURN shared secret을 서버에만 보관하고, 만료시각이 포함된 username을 HMAC-SHA1으로 서명해 시간 제한 TURN 자격증명을 발급한다.
관련 개념: [개념 5 — HMAC과 상수시간 비교] · [개념 21 — 시그널링]
STUN과 TURN의 차이
STUN은 “내가 외부에서 어떤 주소로 보이는가”를 알려준다. TURN은 직접 연결이 안 될 때 서버가 실제 데이터를 릴레이한다. 따라서 TURN은 비용이 들고, 자격증명을 제한해야 한다.
username이 만료시각을 운반한다
user_timestamp_random:expiry 형식의 username은 coturn이 만료 여부를 확인할 수 있게 한다. password는 이 username을 서버 secret으로 HMAC한 값이다.
textusername = user_now_random:expiry password = HMAC-SHA1(TURN_SECRET, username)
브라우저에는 TURN_SECRET이 절대 전달되지 않는다. 브라우저는 발급된 username/password만 받아 ICE server 설정에 넣는다.
실패 경로
만료된 username, 잘못된 HMAC, 허용되지 않은 transport가 들어오면 credential을 거부해야 한다. 검증은 expiry와 HMAC 일치를 각각 확인하며, 비교는 timing leak을 줄이기 위해 constant-time 방식으로 한다.
재구축 시 TURN credential endpoint를 무제한 공개하지 말고, 요청 주체·Origin·발급 빈도·TTL을 함께 제한해야 한다.