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

Memantapkan Mythic.

Selama satu pekan kami berhenti membangun Mythic dan mencoba merusaknya — black-box terhadap server yang hidup, white-box di mana black box tak cukup. Pekan itu menghasilkan dua belas temuan. Semuanya diperbaiki, dipin regression test, dan diverifikasi ulang terhadap reproduksi aslinya sebelum kami menyebutnya selesai. Entri ini juga mencatat dua item roadmap M1 yang mendarat di pass yang sama: rotasi kunci dan store Redis.

Rotasi kunci tanpa downtime

Sampai pekan ini Mythic menandatangani setiap token keputusan dengan satu kunci Ed25519; merotasinya berarti restart dan membatalkan semua token yang masih hidup. Server kini memegang key ring dengan tepat satu signer aktif (ADR-0006). Rotasi berbasis waktu dan berjalan in-process (MYTHIC_KEY_MAX_AGE, default 720h). Kunci retired tetap teriklan di JWKS selama MYTHIC_KEY_RETENTION (default 24h) — sengaja lebih lama daripada usia maksimum token yang pernah ditandatanganinya, sehingga token yang sedang terbang tak pernah rusak di tengah rotasi:

Keystore berupa file JSON yang ditulis atomik; file seed legacy 32-byte tetap bisa dimuat dan bermigrasi saat simpan pertama. Origin tetap memverifikasi token secara lokal, memilih kunci lewat klaim kid — tanpa perlu panggilan koordinasi.

Store yang bertahan lebih dari satu node

Store in-memory mengikat mythicd ke satu node (ADR-0009). MYTHIC_STORE=redis memilih backend Redis: challenge single-use via GETDEL (ambil-dan-hapus atomik) dan counter tekanan sebagai sorted set yang dipangkas satu script Lua. Kedua backend mengimplementasikan counter sliding-window, yang menutup boundary burst jendela tetap. Interface Store kini mengembalikan error: Redis yang tak terjangkau menolak startup, dan outage saat runtime muncul sebagai 503 store_unavailable di /v1 sementara /healthz tetap terbuka untuk load balancer. Satu contract suite bersama dijalankan terhadap kedua backend (memory dan miniredis) agar semantiknya tak bisa melenceng.

Satu catatan, tertulis: store dibagi, kunci penandatanganan tidak. Token dari node A tidak terverifikasi lewat JWKS node B. Verifikasi token lintas node membutuhkan shared keystore (KMS/HSM) — M5, tidak diselesaikan oleh perubahan ini.

Dua belas temuan, dikelompokkan

Tiap baris: apa yang ditunjukkan serangan, lalu apa yang berubah. Setiap perbaikan membawa regression test; setiap perbaikan di-run ulang terhadap reproduksi aslinya.

AreaPenyebabPerbaikan
Transport & limit
Budget rate-limit Budget dikonsumsi setelah validasi — flood invalid-JSON dan site key tak dikenal berjalan tanpa throttling (600 request di 225 req/s, nol 429). Budget kini dihitung sebelum validasi.
Kunci tekanan IPv6 Limiter mengunci IPv6 di /128 — satu /64 mengendalikan 2⁶⁴ identitas (5.000 request dari satu blok, nol tersentuh). Counter kini mengagregasi IPv6 ke /64 (ADR-0007).
Timeout koneksi Tidak ada timeout body/read/idle — klien lambat menahan koneksi tanpa batas. ReadTimeout, WriteTimeout, dan IdleTimeout kini diset.
Ekonomi challenge
Floor solve-time Floor memodelkan worker 50rb H/s; worker crypto.subtle terukur ~400rb H/s — klien jujur ditandai implausible. Floor kini kuantil rendah dari waktu solve jujur (2^d / (2M × 32)): false positive ≤ ~6% kasus terburuk, jawaban kelas GPU memicunya ~96% (ADR-0008).
Verifikasi tertunda Klien mana pun bisa sekadar menunda verifikasi melewati floor berbasis jam dinding — floor tak bisa menangkap kesabaran. Didokumentasikan di threat model sebagai tugas lapisan behavioral (M2), bukan floor.
Token & konfigurasi
TTL token Invarian “token tak pernah hidup lebih lama dari challenge-nya” tak ditegakkan — token 1 jam diterbitkan dari challenge 5 detik. TokenTTL kini di-clamp ke ChallengeTTL.
Clamp skor Tuning jahat (MYTHIC_FAST_SOLVE_PENALTY=500) membocorkan skor risiko 500 ke token ter-tanda-tangan. Skor di-clamp ke 0–100 di batas engine; penalty di atas 100 menolak startup.
Validasi konfigurasi MYTHIC_BASE_DIFFICULTY negatif menerbitkan challenge yang dipenuhi nonce apa pun. Konfigurasi divalidasi saat startup; nilai di luar rentang menolak melayani.
Bentuk kanonis base64 Decoder base64 Go diam-diam membuang low bits tak terpakai dari karakter trailing, sehingga string token berbeda ter-decode ke bytes yang sama dan lolos verifikasi — merusak identitas token di level string. Ketahuan oleh mutation suite tamper-resistance di run pertamanya. VerifyToken menegakkan base64 kanonis: re-encoding bytes hasil decode harus sama persis dengan input, atau token ditolak.
Penanganan state
Konkurensi keystore Dua proses berbagi satu keystore bertabrakan di file temp yang sama atau split-brain kunci signing tanpa suara. Nama temp unik per proses; save menolak menimpa file yang berubah sejak dimuat.
Byte sisa JSON Byte sisa setelah nilai JSON diabaikan diam-diam. Byte sisa kini ditolak.
Saturasi counter Counter sliding-window menyimpan satu entri per hit — termasuk yang ter-rate-limit. Sejuta hit dari satu identitas biayanya ~16,4 MB heap dan 595 dtk CPU. Counter saturasi di IPLimit+1: flood yang sama biayanya +0,00 MB dan 714 ms.

Fix score-clamp layak ditampilkan karena ia duduk di trust boundary — nilai yang akhirnya ditandatangani di dalam token:

// 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
          }

Verifikasi — dan apa yang masih terbuka

Setiap temuan punya reproduksi; setiap fix punya regression test (go test ./... -race, plus typecheck dan test SDK), dan setiap fix diverifikasi ulang dengan reproduksi aslinya. Yang masih terbuka terdokumentasi di threat model, bukan di kepala kami: solver sabar yang menunda verifikasi (M2, sinyal behavioral), serangan terdistribusi dengan identitas segar (sebagian sudah ditangani oleh fingerprint jaringan M1 — lihat 003), dan keystore per-node (M5, KMS/HSM bersama).

— XEY LABS