JA4 핑거프린트는 TLS ClientHello로 계산하는 세 부분짜리 문자열 a_b_c입니다. a는 사람이 읽을 수 있는 메타데이터(프로토콜, TLS 버전, SNI, 암호 스위트와 확장 개수, ALPN), b는 정렬한 암호 스위트 목록의 SHA-256 12자, c는 정렬한 확장과 서명 알고리즘의 SHA-256 12자입니다. b0da82dd1658을 검색해서 오셨다면, 이것은 c 섹션이고 Chromium의 값입니다. 더 정확히는 Chromium 기반 클라이언트가 구 ALPS 확장 코드포인트를 쓰는 상태로 TLS 1.3 세션을 재개할 때 만들어지는 값입니다. 이 글의 나머지에서는 그 근거와, 어떤 JA4 문자열이든 직접 해석하는 방법을 보여 드립니다.
JA4 핑거프린트의 구조: a_b_c 형식
FoxIO의 공식 JA4 기술 명세는 핑거프린트를 사람이 읽을 수 있는 접두부 하나와 잘린 해시 두 개를 밑줄로 이은 것으로 정의합니다. GREASE 값은 모든 곳에서 무시되며, 해시는 모두 소문자입니다.
| 섹션 | 글자 수 | 인코딩하는 정보 | 가능한 값 |
|---|---|---|---|
| a | 1 | 전송 방식 | t = TCP 위의 TLS, q = QUIC, d = DTLS |
| a | 2 | 최고 TLS 버전(supported_versions가 있으면 거기서, 없으면 ClientHello 버전) | 13, 12, 11, 10, s3, d2, 00 = 알 수 없음… |
| a | 1 | SNI 유무 | d = 도메인(SNI 전송), i = SNI 없음(보통 IP로 직접 접속) |
| a | 2 | 암호 스위트 개수(GREASE 제외) | 00–99 |
| a | 2 | 확장 개수(GREASE 제외, SNI와 ALPN 포함) | 00–99 |
| a | 2 | 첫 번째 ALPN 값의 첫 글자와 마지막 글자 | h2, h1(http/1.1), h3, 00 = ALPN 없음 |
| b | 12 | 4자리 hex 암호 스위트를 정렬해 쉼표로 이은 값의 SHA-256 | 없으면 000000000000 |
| c | 12 | 확장(정렬, SNI 0000와 ALPN 0010 제거) + _ + 전송 순서 그대로의 서명 알고리즘의 SHA-256 | 없으면 000000000000 |
사람들이 자주 헷갈리는 부분이 두 가지 있습니다. 첫째, a 섹션의 확장 개수에는 SNI와 ALPN이 포함되지만 c 섹션의 확장 해시에서는 빠집니다. 그래서 같은 클라이언트는 호스트명으로 접속하든 IP로 접속하든 같은 c를 만듭니다. 둘째, 서명 알고리즘은 정렬하지 않고 클라이언트가 보낸 순서 그대로 해시합니다. 그래서 확장이 똑같아도 서명 알고리즘 선호 순서를 바꾸는 클라이언트는 c가 달라집니다.
t13d1516h2_8daaf6152771_… 한 조각씩 해석하기
t: QUIC가 아니라 TCP 위의 TLS.13: 클라이언트가 최고 버전으로 TLS 1.3을 제시.d: SNI 확장이 있으므로 클라이언트는 도메인 이름으로 접속.15: GREASE를 뺀 암호 스위트 15개.16: GREASE를 빼고 SNI와 ALPN을 센 확장 16개.h2: 첫 번째 ALPN 값이h2, 즉 클라이언트가 HTTP/2를 선호.8daaf6152771: 아래의 정렬된 암호 스위트 문자열 해시로, 아무 SHA-256 도구로나 재현할 수 있습니다.
printf '%s' "002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9" \
| sha256sum | cut -c1-12
# 8daaf6152771
이 목록은 TLS 1.3 스위트 3개와 Chrome의 레거시 TLS 1.2 스위트 12개입니다. FoxIO 예제부터 headless Chrome 148까지, 우리가 확인한 모든 Chromium 빌드가 여전히 8daaf6152771을 만듭니다. 따라서 b 섹션은 "Chromium 계열 TLS 스택"이라는 사실을 알려 줄 뿐, 버전에 대해서는 거의 아무것도 알려 주지 않습니다. 버전 차이는 c에서 드러납니다.
명세를 꼼꼼히 읽는다면 작은 주의 사항이 하나 있습니다. 알고리즘 요약에는 t13d1516h2_8daaf6152771_b186095e22b6이 나오지만, 작업 예제를 해시하면 e5627efa2ab1이 나옵니다. 작업 예제를 다시 계산해 보니 e5627efa2ab1이었으므로, 앞의 문자열은 예시용으로 보시면 됩니다.
b0da82dd1658의 의미
b0da82dd1658은 Chromium JA4의 c 섹션입니다. 정확히 다음 문자열의 해시입니다.
0005,000a,000b,000d,0012,0017,001b,0023,0029,002b,002d,0033,4469,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601
직접 확인할 수 있습니다.
printf '%s' "0005,000a,000b,000d,0012,0017,001b,0023,0029,002b,002d,0033,4469,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601" \
| sha256sum | cut -c1-12
# b0da82dd1658
이 목록에서 클라이언트를 특정하는 확장은 세 가지입니다.
0029(pre_shared_key). TLS 1.3 세션 재개입니다. 클라이언트가 같은 서버와의 이전 연결에서 받은 티켓을 이미 갖고 있을 때만 나타납니다. RFC 8446 §4.2.11에 정의되어 있습니다. 이것이 없으면 같은 브라우저의 해시는02713d6af862입니다.4469(ALPS, 원래 코드포인트). Application-Layer Protocol Settings는 Chrome 확장입니다. BoringSSL은 2023년 9월에 새 코드포인트를 추가했고(0x44cd), 현재 Chrome은 새 코드포인트를 보냅니다. 따라서4469는 구버전 Chromium 빌드나 그것을 흉내 내는 도구를 가리킵니다.fe0d(Encrypted Client Hello). Chrome은 일반 연결에서도 GREASE ECH 확장을 보냅니다.
FoxIO의 ja4plus-mapping.csv는 t13d1517h2_8daaf6152771_b0da82dd1658과 t13i1516h2_8daaf6152771_b0da82dd1658을 "Chromium Browser"로 분류합니다. 접두부 계산도 맞아떨어집니다. 해시된 확장 15개에 SNI와 ALPN을 더하면 SNI가 있을 때(d) 17, 없을 때(i) 16이 됩니다. 여기서 온라인에 반복해서 돌아다니는 오류 하나도 드러납니다. t13d1516h2_8daaf6152771_b0da82dd1658 같은 문자열은 실제 ClientHello 하나에서 나올 수 없습니다. SNI와 ALPN이 둘 다 있다면 해시된 확장 15개는 개수 17을 만들어야 하기 때문입니다.
이 값을 직접 재현하기도 했습니다. 2026-10-03, impersonate="chrome131"을 설정한 curl_cffi 0.16.3은 tls.peet.ws로 보낸 첫 요청에서 t13d1516h2_8daaf6152771_02713d6af862로 나타났습니다. 같은 세션의 두 번째 요청은 TLS 세션을 재개했고, t13d1517h2_8daaf6152771_b0da82dd1658로 돌아왔습니다. 요컨대 b0da82dd1658은 "Chromium 형식 ClientHello, 구 ALPS 코드포인트, 재개된 세션"을 뜻합니다. 여기에는 실제 구버전 Chrome, Edge, Brave, Opera 빌드, 임베디드 Chromium, 그리고 전환 이전 Chrome 프로필에 고정된 위장 라이브러리가 모두 해당합니다. JA4만으로는 이들을 구분할 수 없습니다.
참고표: 클라이언트별 실제 JA4 값
아래 값은 두 출처에서만 가져왔습니다. FoxIO가 공개한 예제와 매핑 파일, 그리고 2026-10-03에 tls.peet.ws로 직접 수집한 캡처(버전 명시)입니다. 라이브러리 값은 TLS 백엔드와 빌드 옵션에 크게 좌우되므로, 라이브러리 행은 보편적인 상수가 아니라 예시로 보세요. 직접 수집한 실제 Firefox·Safari 캡처를 포함해 검증할 수 없었던 행은 넣지 않았습니다.
| 클라이언트 | JA4 | 출처 |
|---|---|---|
| Headless Chrome 148(Linux), 새 연결 | t13d1516h2_8daaf6152771_d8a2da3f94cd | 자체 캡처, Playwright Chromium |
| 같은 브라우저, 재개된 세션 | t13d1517h2_8daaf6152771_b6f405a00624 | 자체 캡처 |
| Chromium, 구 ALPS 코드포인트, 새 연결 / 재개 | t13d1516h2_8daaf6152771_02713d6af862 / t13d1517h2_8daaf6152771_b0da82dd1658 | FoxIO 매핑 CSV, curl_cffi chrome131로 재현 |
| QUIC 위의 Chrome | q13d0312h3_55b375c5d22e_178839b6cec1 | FoxIO README |
| Mozilla Firefox(버전 미표기) | t13d1715h2_5b57614c22b0_7121afd63204 | FoxIO 매핑 CSV |
| Safari(버전 미표기) | t13d2014h2_a09f3c656075_14788d8d241b | FoxIO 매핑 CSV |
| curl 8.5.0 / OpenSSL 3.0.13(Ubuntu) | t13d3112h2_e8f1e7e78f70_b26ce05bbdd6 | 자체 캡처, tls.peet.ws·BrowserLeaks·Scrapfly에서 동일 |
| Python requests 2.31 / urllib3 2.0.7, OpenSSL 3.0.13 | t13d3112h1_e8f1e7e78f70_b26ce05bbdd6 | 자체 캡처 |
| Go 1.24.1 net/http(기본 클라이언트) | t13d1311h2_f57a46bbacb6_e7c285222651 | 자체 캡처 |
curl_cffi 0.16.3, impersonate="chrome146" | t13d1516h2_8daaf6152771_d8a2da3f94cd | 자체 캡처 |
curl_cffi 0.16.3, impersonate="firefox147" | t13d1717h2_5b57614c22b0_3cbfd9057e0d | 자체 캡처 |
curl_cffi 0.16.3, impersonate="safari2601" | t13d2013h2_a09f3c656075_7f0f34a4126d | 자체 캡처 |
개별 행보다 패턴이 더 중요합니다.
- c 섹션은 애플리케이션이 아니라 TLS 라이브러리를 식별하는 경우가 많습니다. 같은 OpenSSL 3 빌드 위의 curl과 Python은
b26ce05bbdd6을 공유합니다. FoxIO 매핑 파일에는 접두부는 다르지만 c는 같은 Python 항목이 있습니다. FoxIO 파일의 Go 클라이언트들은 우리의 Go 1.24 캡처와e7c285222651을 공유합니다. - requests는
h1을 보입니다. urllib3 2.x는 ALPN에http/1.1만 알리므로, 서버는 헤더를 하나도 읽기 전에 a 섹션만으로 상대가 브라우저가 아님을 압니다. - 일반 라이브러리는 개수만으로도 쉽게 드러납니다. 암호 스위트 31개(OpenSSL 기본값)나 암호 스위트 13개에 확장 11개(Go)는 현재 모든 Chromium 빌드가 보내는 15/16 패턴과 전혀 다릅니다.
- 세션 재개는 모든 브라우저의 c를 바꿉니다. 우리의 curl_cffi 캡처에서 재개된 세션의 확장 수는 Firefox가 17에서 18로, Safari가 13에서 14로 늘었습니다. 새 연결 값만 허용 목록에 넣은 탐지 규칙은 다시 찾아온 실제 방문자를 잘못 잡게 됩니다.
Chrome 행은 계속 바뀔 것으로 예상하세요. curl_cffi의 최신 chrome150 프로필은 서명 알고리즘 세 개, 0904, 0905, 0906을 추가합니다. IETF 초안의 ML-DSA 코드포인트이며, 이로 인해 c가 806a8c22fdea가 됩니다. 안정 버전 Chrome 150 빌드로는 확인하지 못했으므로 참고값이 아니라 미리보기로 보세요.
JA4가 암호 스위트와 확장을 정렬하는 이유
JA4가 암호 스위트와 확장을 정렬하는 이유는 Chrome이 의도적으로 확장 순서를 무작위화해서, 순서에 민감한 핑거프린트(JA3)가 더 이상 안정적이지 않게 되었기 때문입니다. Chrome은 2023년 초 Chrome 110 무렵 TLS ClientHello 확장 순열을 도입했습니다. 서버와 미들박스가 고정된 배치에 의존하지 못하게 하려는 목적이었습니다. Fastly가 그 영향을 측정한 결과, JA3는 확장을 전송 순서대로 해시하므로 Chrome 설치본 하나가 사실상 연결마다 다른 JA3를 만들었습니다.
JA4는 해시 전에 정렬하는 방식으로 이 문제를 해결합니다. FoxIO는 2023년 9월 JA4+ 제품군의 일부로 이를 공개했습니다(APNIC 재게재). 현재 Cloudflare Bot Management, AWS CloudFront와 WAF, Google Cloud Armor, Fastly, Akamai 등 FoxIO README에 나열된 서비스들이 JA4를 제공합니다.
스크래퍼 입장에서는 양날의 검입니다. 확장 순서를 무작위화해도 더 이상 핑거프린트가 바뀌지 않으므로 그 요령은 끝났습니다. 여전히 중요한 것은 암호 스위트의 집합, 확장의 집합, 그리고 서명 알고리즘의 순서입니다. 실무적으로는 실제 브라우저가 쓰는 TLS 스택을 쓰거나, 그것을 충실히 흉내 내는 라이브러리를 써야 한다는 뜻입니다. 원래 순서 기준의 값이 필요하다면(예: JA3와 비교할 때) 명세가 정의한 JA4_o와 원시 변형 JA4_r / JA4_ro를 쓰면 됩니다. BrowserLeaks는 네 가지를 모두 돌려줍니다. JA3는 TLS 핑거프린팅 해설에서 더 깊이 다룹니다.
JA4+ 제품군: JA4S, JA4H, JA4X, JA4T
JA4는 더 큰 제품군에 속한 방법 하나입니다. 나머지는 서버, HTTP 계층, 인증서, TCP를 보며, 라이선스도 다릅니다.
| 방법 | 대상 | 형식 요약 |
|---|---|---|
| JA4S | 서버의 ServerHello | 프로토콜 + 버전 + 확장 개수 + ALPN, 이어서 선택된 암호 스위트, 이어서 서버 확장의 해시 |
| JA4H | HTTP 요청 | 메서드(ge, po…), 버전(11, 20), 쿠키 c/n, referer r/n, 헤더 개수, Accept-Language 앞 4자, 이어서 순서 그대로의 헤더 이름, 정렬된 쿠키 이름, 정렬된 쿠키 name=value 쌍의 해시 |
| JA4X | X.509 인증서 | 발급자 RDN OID, 주체 RDN OID, 확장 OID의 해시(인증서의 값이 아니라 구성 방식) |
| JA4T | TCP SYN | 윈도 크기, 순서 그대로의 TCP 옵션, MSS, 윈도 스케일. 예: Windows 11은 64240_2-1-3-1-1-4_1460_8 |
형식은 FoxIO의 기술 상세 다이어그램과 참조용 Python 구현에서 가져왔습니다. 스크래핑에서 가장 중요한 것은 JA4H입니다. b 섹션이 헤더 이름을 보낸 순서 그대로 해시하므로, Chrome의 헤더를 잘못된 순서로 보내는 클라이언트는 값이 모두 맞아도 드러납니다. 관련된 SETTINGS와 의사 헤더 순서 신호는 HTTP/2 핑거프린팅 글에서 다룹니다. JA4T는 OS를 드러냅니다. User-Agent가 뭐라고 하든 Linux 서버의 SYN은 Windows처럼 보이지 않습니다.
라이선스
저장소의 라이선스 섹션에 따르면 JA4(TLS 클라이언트 핑거프린트)는 JA3처럼 BSD 3-Clause이며, FoxIO는 이에 대해 특허를 주장하지 않는다고 밝힙니다. JA4S, JA4L, JA4LS, JA4H, JA4X, JA4SSH, JA4T, JA4TS, JA4TScan, JA4D, JA4D6 등 나머지 "JA4+" 방법은 특허 출원 중이며 FoxIO License 1.1을 따릅니다. 이 라이선스는 내부용과 학술용 사용을 허용하지만, JA4+ 핑거프린팅을 제품의 일부로 판매하는 벤더는 OEM 라이선스가 필요합니다.
QUIC와 HTTP/3: q 접두부가 알려 주는 것
q로 시작하는 JA4는 QUIC Initial 패킷으로 계산된 것으로, 클라이언트가 HTTP/3을 설정하고 있었다는 뜻입니다. QUIC는 CRYPTO 프레임 안에 TLS 1.3 ClientHello를 담습니다. Initial 패킷의 보호 키는 클라이언트가 고른 Destination Connection ID에서 도출되므로(RFC 9001 §5.2), 경로상의 관찰자나 엣지 서버는 누구나 이를 복호화해 TCP에서와 똑같이 ClientHello의 핑거프린트를 계산할 수 있습니다.
Chrome의 QUIC 핑거프린트는 TCP 핑거프린트와 매우 다릅니다. FoxIO 예제에서는 q13d0312h3_55b375c5d22e_178839b6cec1입니다. 암호 스위트 3개(QUIC는 구버전을 금지하므로 TLS 1.3만)와 ALPN h3입니다. 자동화에서 이것이 중요한 이유는, 브라우저는 사이트가 HTTP/3을 알리면 HTTP/3으로 옮겨 가지만 대부분의 HTTP 클라이언트는 절대 그러지 않기 때문입니다. Cloudflare가 문서화한 JA4 Signals에는 h2h3_ratio_1h 값이 있습니다. 특정 JA4의 트래픽 중 HTTP/2와 HTTP/3으로 들어오는 비율입니다. HTTP/3 사이트에서 QUIC로 한 번도 나타나지 않는 "Chrome" TLS 핑거프린트는 통계적 이상값입니다. curl_cffi README는 v0.15.0부터 HTTP/3 핑거프린트 위장을 지원한다고 밝힙니다.
내 JA4를 확인하는 방법
가장 빠른 방법은 ClientHello를 읽어 핑거프린트를 돌려주는 에코 서비스입니다. 다음 세 곳은 우리 테스트에서 JA4를 반환했고, 결과가 바이트 단위까지 서로 일치했습니다.
- tls.peet.ws/api/all:
tls.ja4,tls.ja4_r, JA3, HTTP/2 Akamai 핑거프린트,http_version이 담긴 JSON. - tls.browserleaks.com/json:
ja4,ja4_r,ja4_o,ja4_ro가 담긴 JSON. - Scrapfly의 JA3/JA4 도구.
tools.scrapfly.io/api/fp/ja3의 API가ja4와ja4_r을 반환합니다.
curl -s https://tls.peet.ws/api/all | python3 -c \
"import sys, json; d = json.load(sys.stdin); print(d['http_version'], d['tls']['ja4'])"
같은 확인을 노트북에서만 하지 말고 프록시를 거쳐서도 해 보세요. CONNECT로 터널링하는 포워드 프록시는 ClientHello를 그대로 통과시키지만, TLS를 가로채는 프록시, 기업용 미들박스, 일부 SDK 계층은 자기 것으로 바꿔 끼웁니다. 예를 들어 ProxyHat 레지덴셜 출구로는 다음과 같습니다.
curl -s -x http://USERNAME:PASSWORD@gate.proxyhat.com:8080 https://tls.peet.ws/api/all
Wireshark와 tshark
TLS 필드 레퍼런스에 따르면 Wireshark는 4.2.0부터 디스플레이 필터 필드 tls.handshake.ja4(와 tls.handshake.ja4_r)로 JA4를 기본 계산합니다. FoxIO의 JA4+ 플러그인(Wireshark 4.4.0 이상)은 ja4.ja4s, ja4.ja4h, ja4.ja4t 등 제품군의 나머지를 ja4.* 아래에 추가합니다. 캡처에서 핑거프린트를 뽑아내려면:
tshark -r capture.pcapng -Y "tls.handshake.ja4" -T fields \
-e ip.dst -e tls.handshake.extensions_server_name -e tls.handshake.ja4
클라이언트를 직접 통제할 수 있는 머신에서 캡처하세요. headless 브라우저의 HTTPS 트래픽을 핑거프린팅하려면 로컬에서 실행하고 루프백이나 외부 인터페이스에서 캡처하면 됩니다. ClientHello는 암호화되지 않으므로 JA4에는 TLS 키가 필요 없습니다. HTTPS에서 JA4H를 보려면 키가 필요합니다.
실무적 의미: 헤더는 핸드셰이크와 맞아야 합니다
User-Agent를 바꿔도 JA4는 바뀌지 않습니다. Chrome User-Agent를 보내는 Python 스크립트도 헤더가 하나라도 읽히기 전, 핸드셰이크에서 여전히 t13d3112h1_e8f1e7e78f70_b26ce05bbdd6을 보냅니다. Chrome이라고 주장하면서 OpenSSL처럼 협상하고 HTTP/1.1을 요청하는 이 불일치는 엣지가 돌릴 수 있는 가장 저렴한 검사 중 하나입니다. JA4 데이터베이스가 잡아내도록 만들어진 것도 정확히 이것입니다.
주류 해결책은 두 가지이며, 둘 다 한계가 있습니다.
- curl_cffi(curl-impersonate의 Python 바인딩)는 지정한 브라우저 프로필에 맞춰 ClientHello와 HTTP/2 프레임을 구성합니다:
requests.get(url, impersonate="chrome"). 우리 캡처에서 현재 Chrome 프로필은 실제 headless Chrome 148과 같은 JA4를 만들었습니다. 세션과 프록시는 curl_cffi 가이드에서 다룹니다. - Go용 uTLS는
tls.UClient(conn, &config, helloID)를 통해crypto/tls의 ClientHello를tls.HelloChrome_Auto같은 "parrot"으로 대체합니다. README는 흉내가 불완전할 수 있고 ClientHello 너머로는 확장되지 않는다고 분명히 밝힙니다.net/http에 연결하는 방법은 Go uTLS 실습 가이드에서 보여 드립니다.
위장으로 해결되지 않는 것
JA4가 일치해도 첫 번째 필터를 통과할 뿐입니다.
- JA4는 주장하는 브라우저 버전과도 맞아야 합니다. Chrome 148 User-Agent에 전환 이전 ALPS 코드포인트(
b0da82dd1658)가 붙으면 그 자체로 작은 불일치입니다. - JA4H 헤더 순서도 맞아야 합니다.
- JA4T와 IP 데이터는 호스트 OS와 네트워크를 드러냅니다.
- JavaScript 챌린지는 HTTP 클라이언트에서 실행되지 않습니다.
- 행동(요청 속도, 탐색 패턴)은 별도로 평가됩니다.
보내는 User-Agent와 맞는 프로필을 고르고, 세션 재개가 자연스럽게 보이도록 세션을 유지하고, 출구 IP가 트래픽에 걸맞게 그럴듯하도록 하세요. 레지덴셜 출구는 네트워크 평판 계층을 해결할 뿐 TLS 계층은 해결하지 않습니다. 위에서 보여 드린 것처럼 프록시를 거쳐 핑거프린트를 확인하세요.
마지막으로, 핑거프린트를 이해하는 목적은 정당한 자동화를 정직하고 예측 가능하게 만드는 데 있지, 자동화 접근을 거부하기로 한 사이트를 뚫는 데 있지 않습니다. 해당되는 경우 robots.txt와 이용약관을 존중하고, 개인정보 스크래핑에는 완벽한 JA4로도 해결되지 않는 법적 의무가 따른다는 점을 기억하세요.






