三つの段階で、必要な分だけ確認する
クライアントは判定をしません。生の特徴量だけを送り、サーバーが毎回スコアを再計算します。
観測
SDK がページ上の操作(カーソルの加速度やジッター、タッチの接触面、キー入力の間隔)と、Canvas / WebGL 描画や自動化ツールの痕跡を受動的に集めます。キーの内容や生の IP は送りません。
リスク判定
環境・行動・ネットワーク・履歴の 4 系統を 0〜1 に正規化し、noisy-OR で統合して 0.0〜1.0 のスコアにします。WebDriver のような決定的な信号は最低レベルを引き上げます。
課題または通過
スコアに応じて Level 0〜3 を選び、Level 0 ならそのまま単回使用のトークンを発行します。課題は毎回生成され、解答だけでなく解いている間の操作も検証します。
リスクに比例した難しさ
既定の閾値は 0.20 / 0.50 / 0.80。ログインや決済など重要なアクションでは閾値を 0.05 下げ、課題に失敗すると次はひとつ上のレベルになります。
| Level | 条件 | 利用者が体験すること | 目安 |
|---|---|---|---|
| L0 Pass | score < 0.20、1.5 秒以上の観測または良好な履歴 | 何も表示されない。送信と同時にトークンが付く。 | 0 秒 |
| L1 Low | 0.20 〜 0.50、または行動の証拠が無い | アイコンのタップ、スライダーパズルなど 1 ステップ。 | 2〜3 秒 |
| L2 Medium | 0.50 〜 0.80、または自動化痕跡 | 敵対的ノイズ付きの画像選択、3D 回転、軌跡なぞり、並べ替え。 | 4〜6 秒 |
| L3 High | 0.80 以上、TLS 指紋と UA の不一致など | 2 段の連続課題。各段で操作の人間らしさも検証。 | 10〜15 秒 |
九つの課題、すべて使い捨て
画像も音声も毎回生成し、配信 URL は一度しか使えません。分類だけできる AI には解けない「向き」や「大小」の関係を問います。
アイコンタップ
ノイズ背景の中から指定した図形をひとつタップ。
スライダーパズル
欠けたピースを正しい位置までスライド。ドラッグの軌跡も評価。
3D 回転
立方体をドラッグして参照画像と同じ向きに。角度差で判定。
敵対的ノイズ画像選択
幾何歪みと周波数領域の摂動を加えた 9 枚から「左を向いた動物」などを選ぶ。
軌跡なぞり
曲線を始点から終点までなぞる。Fréchet 距離で一致を判定。
並べ替え
大きさの異なる円を小さい順に並べる。キーボード操作にも対応。
ミニゲーム
動く的を 3 回タップ。タップ時刻と位置をサーバーが再計算。
ノイズ混入音声
背景雑音の中の短いビープ音を数える。視覚課題の代替。
ローカル論理問題
画像を配信できない低帯域環境向けの短い設問。
四つの系統を独立に見て、ひとつでも強ければ疑う
環境フィンガープリント
navigator.webdriver、ChromeDriver / Selenium / Playwright のグローバル変数、CDP の Runtime.enable 痕跡、ネイティブ関数の差し替え、SwiftShader などのソフトウェア描画、Canvas と WebGL の描画結果。
行動バイオメトリクス
カーソルの直線性・ジッター・速度変動・イベント間隔の規則性・3 次ベジェ適合残差(滑らかすぎる軌跡は生成器の疑い)、タッチの接触半径と圧力の分散、キー入力の Dwell / Flight time の変動係数。
ネットワーク
データセンター / VPN / Proxy / Tor の判定、JA3 / JA4 の TLS 指紋と申告 UA の整合、HTTP/2 指紋、IP /24・サイト・指紋ハッシュの 3 段レートリミット。
レピュテーション
日次ローテーションのソルト付きハッシュだけで保持する失敗率・超人的な解答速度・近傍 IP の Bot 比率。良好な履歴はスコアを割り引きます。
再利用できるものを残さない
- 課題トークン・通過トークンは HMAC-SHA256 の JWT。
jtiは単回消費、有効期限は 120〜300 秒。 - IP /24・UA ハッシュ・サイトキーにトークンを束縛。別端末からの提示は拒否。
- 解答そのものはサーバーに持たず、コミットメントだけを照合。
- 正答でも、課題中の操作が機械的なら不合格。不可視のハニーポットに触れた場合も同様。
- 判定理由はクライアントに返さない。攻撃者への勾配情報を与えないため。
集めない、残さない
- Cookie・localStorage を使わない。同意バナーは不要。
- 生の IP アドレスはログにも残さない。/24 に丸めて記録。
- キー入力はタイミングのみ。文字は収集しない。
- 操作の軌跡は判定後に破棄。残るのは匿名の集計値のみ。
- 音声課題の音は端末外へ送らない。
フロントエンドに 3 行、バックエンドに 1 リクエスト
フロントエンド(ES Module)
import { TaruCaptcha } from "https://captcha.taruta.run/sdk/tc.js";
const tc = await TaruCaptcha.init({ siteKey: "tc_…", action: "login" });
form.addEventListener("submit", async (e) => {
e.preventDefault();
form.taru_token.value = await tc.execute(); // Level 0 なら UI なし
form.submit();
});
バックエンド(サーバー間)
curl -X POST https://captcha.taruta.run/api/v1/siteverify \
-H "Authorization: Bearer $SITE_SECRET" \
-H "Content-Type: application/json" \
-d '{"pass_token":"…","remote_ip":"203.0.113.5","expected_action":"login"}'
{"success":true,"score":0.07,"level_served":0,"action":"login",
"hostname":"captcha.taruta.run","challenge_ts":"2026-10-03T08:00:12Z","error_codes":[]}
エンドポイント
- GET
/api/v1/session/init— ナンスと鍵素材の発行(10 分、単回) - POST
/api/v1/challenge/request— メトリクス送信、Level と課題または通過トークン - POST
/api/v1/challenge/verify— 解答と課題中の操作を検証 - POST
/api/v1/siteverify— サイト側バックエンドが通過トークンを単回消費
エラーは RFC 9457 Problem Details(application/problem+json)で返します。409 token-already-used、410 challenge-expired、422 context-mismatch、429 rate-limited。ブラウザ向けのページは /errors/ にまとめています。