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)として扱う:小さく、厳密に上限のある寄与で、 リクエストの価格を引き上げはできるが、単独で無罪にも有罪にも できない。フローはこうなる:
図1 —— すべてのシグナルは参考扱いで上限付き。単独のシグナルがリクエストを無罪にも有罪にもできない。
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=1 | 7/7 パッケージ、データレース 0 |
bun test (JS SDK) | 17/17 |
tsc --noEmit · bun run build | クリーン |
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完了」宣言の後、ゼロからの監査(フレッシュクローン、全スイート
再実行、検証パスへの敵対的パス)が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