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

M1 完了: ネットワークシグナル、Worker ソルバー、オリジン SDK。

M1は、「適応的」が Mythic のREADMEの形容詞であることをやめ、メカニズムになったマイルストーンだ。 これまでサーバーはプルーフオブワークの難易度という1つの道具だけで 悪用に価格を付けていた。M1は、誰が要求しているかに価格を付けられる シグナルを加える:JA4 TLSフィンガープリントとASNレピュテーションが 上限付きリスクエンジンに入力され、PoWソルバーがメインスレッドから 外れ、オリジンはNode.jsとPythonで判定をネイティブ検証できる。 コミット bc87ed9 から c7e4fc3。

本記事は、元の「M1完了」宣言に7つの穴があることが内部監査で 見つかったため、改訂された。6件はコミット 067e9f7 で修正済み;1件は正直に未解決のままで、以下に 記載する。修正はここに文書化されており、埋もれさせない。

なぜシグナルか —— そしてなぜ参考扱いなのか

ボット対策はシグナルで生き死にし、単独のシグナルはどれも弱い。 JA4フィンガープリントは、手間を惜しまないクライアントなら誰でも バイト単位で真似できる。ASNはヒューリスティック —— 今日は データセンター、明日は住宅プロキシだ。どちらかを絶対真理として 扱えば、手の込んだブロックリストを作ったことになる。

だからM1では、すべてのネットワークシグナルを参考情報 (advisory)として扱う:小さく、厳密に上限のある寄与で、 リクエストの価格を引き上げはできるが、単独で無罪にも有罪にも できない。フローはこうなる:

JA4フィンガープリント:割引は上限付き、信頼はしない

ADR-0010。 サーバーはJA4フィンガープリントを X-Mythic-JA4 から読む。これは終端プロキシが設定する —— クライアントから直接は読まない。クライアントが提供する値は攻撃者が 制御できるからだ。フィンガープリントは既知ブラウザのJA4パターンと 照合され、一致すれば小さなリスク割引が適用される。割引には上限が あり、単独でチャレンジを解除できない。形式が壊れた値やプレフィックス のみの一致は、割引パスから完全に除外される (a8ce95a)。すべての上限は起動時に検証され、敵対的な チューニング値はサーブを拒否する (8f560bd)。

明示した制限:JA4は参考情報だ。実在ブラウザのフィンガープリントを バイト単位で真似するクライアントは、このレイヤーでは区別できない。 割引が小さいのは、まさにこのシグナルが偽装可能だからだ —— 素朴な スクリプティングのコストを引き上げるだけで、それ以上ではない。

ASNスコアリング:失敗時は閉じるヒューリスティック

同じADR。クライアントIPはMaxMind GeoLite2-ASNデータベース (MYTHIC_ASN_DB) 経由で自律システム番号(ASN)に解決される。 データセンター、ホスティング、VPN出口のASNはリスクに寄与し、住宅ISPの ASNは寄与しない。ルックアップは全体でnilセーフ:空の MYTHIC_ASN_DB はシグナルをクリーンに無効化し、設定された パスが開けない場合は起動時に致命的エラーになる。設定ミスは閉じる 方向に失敗し、未設定は設計上静かに劣化する —— 運用者が有効化しない ことを選んだのだから。

監査による修正:元のテストは失敗モード(nil、空パス、 不正パス、無効IP)しかカバーしておらず、肯定的なルックアップは一度も 実行されていなかった。067e9f7 はリポジトリ内で生成した 合成 .mmdb フィクスチャ(ライセンス付きMaxMindデータでは ない)に対する TestASNLookupPositive を追加し、正常系を 固定した:データセンター範囲はスコアし、住宅範囲はスコアしない。

Web Workerソルバー:PoWをメインスレッドから外す

SHA-256の演算は以前メインスレッドで動いていて、難易度24以上では ページが数秒固まった。SDKは solveInWorker() を出荷する。 これは自己完結型のソルバーをBlob URLに文字列化してスレッド外で実行 する。デプロイするWorkerファイルも、バンドラープラグインも不要。 進捗コールバックは2048イテレーションごとに発火し、 AbortSignal はWorkerを停止して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)を取り、構築時に行動コレクターを自動開始する。 クライアントは1つ、フローは1つ、手動の配線は不要。

正直に明示する既知の穴:WorkerパスはBunのWorker実装でしか 動いたことがなく、Chromium・Firefox・Safariでは一度も実行されていない。 これは sdk/js/README.md に文書化されており、そうでない とは主張していない。Playwrightスイートを試みたが、ビルド環境が ブラウザバイナリを取得できなかった。それが変わるまで、実ブラウザの 挙動は未テストであり、主張もしない。

オリジン検証:Node.jsとPython

決定トークンはGo (server/xtoken) でしか検証できなかった。 オリジンはネイティブヘルパーを得た。 sdk/js/src/verify.ts —— parsePublicKey(x) はJWKSの生鍵をDER SPKIでラップし、 verifyToken(pub, token) は生ペイロードバイトに対する Ed25519署名を検証し(事前ハッシュなし)、正準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で同一に検証する(3言語とも dec=allow risk=6)。

エビデンス

スイート結果
go build ./... · go vet ./...クリーン
go test -race -count=17/7 パッケージ、データレース 0
bun test (JS SDK)17/17
tsc --noEmit · bun run buildクリーン
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完了」宣言の後、ゼロからの監査(フレッシュクローン、全スイート 再実行、検証パスへの敵対的パス)が7つの穴を見つけた。コミット 067e9f7("fix(audit): close all 7 M1 audit gaps")が そのうち6つを閉じた:

  • ASNの肯定的テストを追加(失敗モードのみだった)。
  • 行動ヒントは現在 verify() で送信される —— 以前はコレクターがチャレンジ後に停止し、サーバー側のverify段階の行動解析が実質的にデッドコードになっていた。
  • Python SDKをCIと make test に追加。
  • 言語横断E2Eをコミット (e2e/crosslang.sh)。
  • docs/ROADMAP.md を修正 —— 完了済みM1項目が未計画のままだった。
  • 誤解を招くWorkerテスト名を修正(フォールバックをテストすると主張しながら実際は本物のWorkerパスをテストしていた)。

7つ目 —— Web Workerの実ブラウザテスト —— は未解決のまま、完了と 主張せず既知の穴として文書化されている。それが現在のM1の正直な状態だ。

未解決の問い

実ブラウザでのWorkerカバレッジが上記で述べた1件の未解決項目だ。それ以外 では:IPレピュテーションフィードは依然計画中(ROADMAP、正直に記載)、 M2の行動バイオメトリクスとM5のクロスノード検証用共有キーストアが次の マイルストーンとして残る。プロジェクトルールとして、すべての新しい シグナルは出荷前にバイパスを文書化する —— ADR-0010の「Bypasses」節を参照。

— XEY LABS