M1 완료: 네트워크 시그널, Worker 솔버, 오리진 SDK.
M1은 “적응형”이
Mythic의
README에 적힌 형용사에서 메커니즘이 된 마일스톤입니다. 지금까지
서버는 하나의 수단으로 남용에 가격을 매겼습니다 —— 작업증명
난이도. M1은 누가 묻는지에 가격을 매길 수 있게 하는
시그널을 추가합니다: JA4 TLS 지문과 ASN 평판이 캡핑된 리스크
엔진에 들어가고, PoW 솔버가 메인 스레드를 떠나며, 오리진이
Node.js와 Python에서 판정을 네이티브로 검증할 수 있습니다. 커밋
bc87ed9부터 c7e4fc3까지.
이 글은 내부 감사에서 원래의 “M1 완료” 주장에 일곱 개의
공백을 발견한 후 수정되었습니다. 여섯 개는 커밋
067e9f7에서 수정되었고, 하나는 솔직히 열린 채로
남아 있으며 아래에 적혀 있습니다. 수정 사항은 묻지 않고 여기에
문서화되어 있습니다.
시그널이 왜 필요한가 —— 그리고 왜 권고용만인가
봇 완화는 시그널에 사느냐 죽느냐가 걸려 있고, 단독의 모든 시그널은 약합니다. JA4 지문은 조금만 신경 쓰면 어떤 클라이언트든 바이트 단위로 흉내 낼 수 있습니다. ASN은 휴리스틱입니다 —— 오늘은 데이터센터, 내일은 주거용 프록시. 둘 중 하나를 진실로 다루면, 단계를 더한 블록리스트를 만든 것입니다.
그래서 M1은 모든 네트워크 시그널을 권고용으로 다룹니다: 작고 하드캡된 기여로, 요청의 가격을 올릴 수는 있지만 단독으로 통과시키거나 정죄할 수는 없습니다. 흐름은 이렇습니다:
그림 1 — 모든 시그널은 권고용이며 캡핑되어 있습니다. 어떤 단일 시그널도 요청을 단독으로 통과시키거나 정죄할 수 없습니다.
JA4 핑거프린팅: 할인, 캡핑, 불신
ADR-0010.
서버는 종료 프록시가 설정한 X-Mythic-JA4에서 JA4
지문을 읽습니다 —— 클라이언트에서 직접 절대 받지 않습니다.
클라이언트가 제공한 값은 공격자가 제어하므로. 지문은 알려진
브라우저 JA4 패턴과 매칭됩니다; 매칭되면 작은 리스크 할인을
적용하며, 챌린지를 단독으로 통과시킬 수 없도록 캡핑됩니다. 형식이
잘못된 값과 접두사만 일치하는 경우는 할인 경로에서 완전히
거부됩니다 (a8ce95a). 모든 캡은 시작 시 검증되며,
적대적 튜닝 값은 서비스를 거부합니다 (8f560bd).
명시된 한계: JA4는 권고용입니다. 실제 브라우저의 지문을 바이트 단위로 흉내 내는 클라이언트는 이 레이어에서 구분할 수 없습니다. 할인이 작은 이유는 바로 시그널이 위조 가능하기 때문입니다 —— 순진한 스크립팅의 비용을 올릴 뿐, 그 이상은 아닙니다.
ASN 스코어링: 닫힌 방향으로 실패하는 휴리스틱
같은 ADR. 클라이언트 IP는 MaxMind GeoLite2-ASN 데이터베이스
(MYTHIC_ASN_DB)를 통해 Autonomous System Number로
해석됩니다. 데이터센터, 호스팅, VPN 출구 ASN은 리스크에
기여하고; 주거용 ISP ASN은 기여하지 않습니다. 조회는 전역적으로
nil-safe합니다: 빈 MYTHIC_ASN_DB는 시그널을
깔끔하게 비활성화하고, 열기에 실패하는 설정된 경로는 시작 시
치명적입니다. 설정 오류는 닫힌 방향으로 실패합니다; 부재는
설계상 조용히 저하됩니다 —— 운영자가 활성화하지 않기로 선택한
것입니다.
감사 수정: 원래 테스트는 실패 모드만 커버했습니다
(nil, 빈 경로, 잘못된 경로, 유효하지 않은 IP) —— 정상 조회는
한 번도 실행되지 않았습니다. 067e9f7은 저장소 안에서
생성한 합성 .mmdb 픽스처에 대한
TestASNLookupPositive를 추가했습니다 (라이선스된
MaxMind 데이터가 아님). 해피 패스를 고정합니다: 데이터센터
범위는 스코어하고, 주거용 범위는 하지 않습니다.
Web Worker 솔버: 메인 스레드를 떠난 PoW
SHA-256 연산은 이전에 메인 스레드에서 돌았습니다 —— 난이도 24
이상에서는 페이지가 몇 초 동안 멈췄습니다. SDK는 이제
solveInWorker()를 제공합니다: 자체 완결형 솔버를
Blob URL로 문자열화해 오프스레드에서 실행합니다. 배포할 별도의
워커 파일도 없고, 번들러 플러그인도 필요 없습니다. 진행 콜백은
2048 반복마다 발사되고, AbortSignal은 워커를
종료하고 URL을 해제합니다:
export async function solveInWorker(
challenge: Challenge,
opts: SolveInWorkerOptions = {},
): Promise<Solution> {
// No Worker API (Node, SSR) → fall back to main thread.
if (typeof Worker === "undefined") {
const { solve } = await import("./pow");
return solve(challenge, opts);
}
const workerCode = `(${workerMain.toString()})()`;
const blob = new Blob([workerCode], { type: "application/javascript" });
const url = URL.createObjectURL(blob);
// …postMessage challenge, await solution, revoke URL
}
MythicClient는 useWorker를 받으며
(기본값 true), 생성 시 행동 수집기를 자동으로
시작합니다. 하나의 클라이언트, 하나의 흐름, 수동 연결 없음.
알려진 공백, 솔직히 말하면: 워커 경로는 지금까지
Bun의 Worker 구현에서만 실행됐습니다 —— Chromium, Firefox,
Safari에서는 한 번도 없습니다. 이는
sdk/js/README.md에 문서화되어 있고, 그 외의
주장은 하지 않습니다. Playwright 스위트를 시도했지만, 빌드
환경이 브라우저 바이너리를 가져올 수 없었습니다. 그게 바뀌기
전까지 실제 브라우저 동작은 테스트되지 않았고 주장하지도
않습니다.
오리진 검증: Node.js와 Python
판정 토큰은 Go (server/xtoken)에서만 검증
가능했습니다. 오리진은 이제 네이티브 헬퍼를 받습니다.
sdk/js/src/verify.ts ——
parsePublicKey(x)는 JWKS raw 키를 DER SPKI로
감싸고; verifyToken(pub, token)은 raw 페이로드
바이트에 대한 Ed25519 서명을 확인하고 (사전 해시 없음),
canonical base64url 재인코딩을 강제하며, 만료된 토큰을
거부합니다:
// Canonical encodings only: re-encoding the decoded bytes
// must reproduce the input exactly. Go's base64 decoder
// silently discards unused low bits of a trailing character,
// so distinct strings can decode to the same token.
if base64.RawURLEncoding.EncodeToString(payload) != payloadPart ||
base64.RawURLEncoding.EncodeToString(sig) != sigPart {
return nil, fmt.Errorf("%w: non-canonical encoding", ErrInvalidToken)
}
sdk/python/mythic/verify.py ——
cryptography 패키지를 통한 동일한 의미:
parse_public_key, verify_token,
TokenClaims 데이터클래스, 모든 거부 시
TokenError. Python SDK는 이제 CI에 있으며
(.github/workflows/sdk-python-ci.yml)
make test 하에서 실행됩니다 —— 이전에는 아무것도
자동으로 실행하지 않았습니다.
크로스 언어 검증 후 커밋: e2e/crosslang.sh가 라이브
Go 서버에서 토큰을 발행하고 Go, Python, JS 세 곳에서 동일하게
검증합니다 (dec=allow risk=6, 세 곳 모두).
증거
| 스위트 | 결과 |
|---|---|
go build ./... · go vet ./... | clean |
go test -race -count=1 | 7/7 패키지, 0 data race |
bun test (JS SDK) | 17/17 |
tsc --noEmit · bun run build | clean |
pytest (Python SDK) | 6/6 |
e2e/crosslang.sh | Go, Python, JS가 라이브 토큰에 동의 |
스코어링 계약 자체가 코드에 고정되어 있습니다 —— 적대적 튜닝은 서명된 토큰에 범위를 벗어난 점수를 누출할 수 없습니다 (레드팀 발견 H3, 회귀 테스트됨):
// The score is a 0-100 contract (token claim `risk`,
// dashboards, tuning docs). Hostile tuning must clamp
// here, not leak 500 into tokens (red-team H3).
if score > 100 {
score = 100
}
if score < 0 {
score = 0
}
감사가 바꾼 것
원래의 “M1 완료” 주장 이후, 처음부터 다시 한 감사 (새 클론, 모든
스위트 재실행, verify 경로에 대한 적대적 패스)가 일곱 개의 공백을
발견했습니다. 커밋 067e9f7 (“fix(audit): close all 7
M1 audit gaps”)이 그 중 여섯 개를 닫았습니다:
- ASN positive 테스트 추가 (실패 모드만 있었던 것을 수정).
- 행동 힌트가 이제
verify()시점에 전송됨 —— 이전에는 수집기가 챌린지 이후 멈춰, 서버의 verify 단계 행동 파싱이 사실상 데드 코드였다. - Python SDK를 CI와
make test에 추가. - 크로스 언어 E2E 커밋 (
e2e/crosslang.sh). docs/ROADMAP.md수정 —— 완료된 M1 항목이 여전히 계획 중으로 표시되어 있었음.- 오해의 소지가 있는 워커 테스트 이름 수정 (실제 워커 경로를 테스트하면서 폴백을 테스트한다고 주장했다).
일곱 번째 —— Web Worker에 대한 실제 브라우저 테스트 —— 는 열린 채로 남아 있으며, 완료된 것으로 주장하는 대신 알려진 공백으로 문서화되어 있습니다. 이것이 M1의 현재 솔직한 상태입니다.
열린 질문
실제 브라우저 워커 커버리지가 위의 유일한 열린 항목입니다. 그 너머: IP 평판 피드는 여전히 계획 중이며 (ROADMAP, 솔직히 표시됨), M2 행동 바이오메트릭과 크로스 노드 검증을 위한 M5 공유 키스토어가 다음 마일스톤으로 남아 있습니다. 프로젝트 규칙에 따라, 모든 새 시그널의 바이패스는 시그널이 출시되기 전에 문서화됩니다 —— ADR-0010 § “Bypasses” 참조.
— XEY LABS