XEY LABS — MYTHIC / 003 EN/ ID/ 中文/ 日本語/ 한국어 ← BUILD LOG PROJECT REPO ↗ GITHUB ↗

M1 완료: 네트워크 시그널, Worker 솔버, 오리진 SDK.

M1은 “적응형”이 Mythic의 README에 적힌 형용사에서 메커니즘이 된 마일스톤입니다. 지금까지 서버는 하나의 수단으로 남용에 가격을 매겼습니다 —— 작업증명 난이도. M1은 누가 묻는지에 가격을 매길 수 있게 하는 시그널을 추가합니다: JA4 TLS 지문과 ASN 평판이 캡핑된 리스크 엔진에 들어가고, PoW 솔버가 메인 스레드를 떠나며, 오리진이 Node.js와 Python에서 판정을 네이티브로 검증할 수 있습니다. 커밋 bc87ed9부터 c7e4fc3까지.

이 글은 내부 감사에서 원래의 “M1 완료” 주장에 일곱 개의 공백을 발견한 후 수정되었습니다. 여섯 개는 커밋 067e9f7에서 수정되었고, 하나는 솔직히 열린 채로 남아 있으며 아래에 적혀 있습니다. 수정 사항은 묻지 않고 여기에 문서화되어 있습니다.

시그널이 왜 필요한가 —— 그리고 왜 권고용만인가

봇 완화는 시그널에 사느냐 죽느냐가 걸려 있고, 단독의 모든 시그널은 약합니다. JA4 지문은 조금만 신경 쓰면 어떤 클라이언트든 바이트 단위로 흉내 낼 수 있습니다. ASN은 휴리스틱입니다 —— 오늘은 데이터센터, 내일은 주거용 프록시. 둘 중 하나를 진실로 다루면, 단계를 더한 블록리스트를 만든 것입니다.

그래서 M1은 모든 네트워크 시그널을 권고용으로 다룹니다: 작고 하드캡된 기여로, 요청의 가격을 올릴 수는 있지만 단독으로 통과시키거나 정죄할 수는 없습니다. 흐름은 이렇습니다:

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=17/7 패키지, 0 data race
bun test (JS SDK)17/17
tsc --noEmit · bun run buildclean
pytest (Python SDK)6/6
e2e/crosslang.shGo, 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