7
Verifikasi Host Key TOFU
Setiap koneksi SSH-family di Termimus melewati satu gerbang pemeriksaan host key: SshClientHandler. Handler ini membandingkan fingerprint SHA-256 dari public key yang dipresentasikan server dengan entri yang tersimpan di tabel known_hosts, memakai model Trust On First Use (TOFU). Selama fingerprint cocok, koneksi lanjut; begitu fingerprint berubah untuk alamat dan port yang sama, koneksi ditolak. README menempatkan mekanisme ini sebagai bagian dari jaminan keamanan lokal aplikasi dan menyebutnya secara eksplisit sebagai perlindungan terhadap serangan Man-in-the-Middle. README.md:881 README.md:2192
Handler didefinisikan sebagai struct biasa yang membawa address, port, dan db: Arc<Database>, lalu mengimplementasikan trait Handler dari russh lewat method check_server_key. Komentar pada definisinya menyatakan bahwa penolakan dilakukan langsung bila key berubah untuk alamat:port yang sebelumnya sudah dipercaya, setara dengan perlindungan berkas known_hosts milik OpenSSH. src-tauri/src/ssh/mod.rs:47-563
Satu gerbang untuk semua koneksi SSH
SshClientHandler tidak dipakai di satu tempat saja. Empat jalur koneksi membangun handler yang sama dengan db yang sama, sehingga tidak ada jalur yang bisa melewati verifikasi.
graph LR
term["SshManager::connect<br/>target dan jump host"]
sftp["SftpManager::connect"]
tun["TunnelManager::start"]
detect["detect_os_via_exec"]
handler["SshClientHandler.check_server_key"]
get["Database.get_known_host"]
save["Database.save_known_host"]
kh[("known_hosts")]
term --> handler
sftp --> handler
tun --> handler
detect --> handler
handler --> get
get --> kh
handler --> save
save --> kh
style handler fill:#f59e0b,color:#000
style get fill:#8b5cf6,color:#fff
style save fill:#8b5cf6,color:#fff
style kh fill:#336791,color:#fff
style term fill:#2d7d46,color:#fff
style sftp fill:#2d7d46,color:#fff
style tun fill:#2d7d46,color:#fff
style detect fill:#2d7d46,color:#fff
%% fig: Empat jalur koneksi menuju satu pemeriksaan host key
%% desc: Terminal, SFTP, tunnel, dan deteksi OS semuanya membangun SshClientHandler yang membaca serta menulis tabel known_hosts.Keempat pemanggil itu adalah handshake target di SshManager::connect beserta handshake jump host di method yang sama, koneksi SFTP di SftpManager::connect, pembukaan tunnel di TunnelManager::start, dan koneksi latar untuk detect_os_via_exec. Pada SFTP, handler dibangun sebelum client::connect_stream dipanggil, sehingga kegagalan verifikasi muncul sebagai error koneksi SSH. src-tauri/src/ssh/mod.rs:464-4814 src-tauri/src/ssh/mod.rs:369-3755 src-tauri/src/sftp/mod.rs:67-82 src-tauri/src/tunnel/mod.rs:62-77 src-tauri/src/ssh/mod.rs:853-860
Tiga cabang keputusan
check_server_key menerima server_public_key, membentuk fingerprint dengan HashAlg::Sha256, mencatat key_type dari server_public_key.algorithm(), lalu mengambil entri lama lewat self.db.get_known_host(&self.address, self.port). Hasilnya bercabang menjadi tiga: entri belum ada, entri ada dan fingerprint sama, atau entri ada dan fingerprint berbeda. src-tauri/src/ssh/mod.rs:61-1009 src-tauri/src/ssh/mod.rs:65-6710
sequenceDiagram
participant R as russh
participant H as SshClientHandler
participant DB as Database
participant KH as known_hosts
R->>H: check_server_key server_public_key
H->>H: fingerprint HashAlg::Sha256 dan key_type
H->>DB: get_known_host address port
DB->>KH: SELECT by PRIMARY KEY address port
KH-->>DB: baris atau kosong
DB-->>H: Option KnownHost
alt entri belum ada
H->>DB: save_known_host entri baru
DB->>KH: INSERT ON CONFLICT UPDATE
H-->>R: Ok true
else fingerprint sama
H->>DB: save_known_host last_seen_at baru
H-->>R: Ok true
else fingerprint berbeda
H-->>R: Err REMOTE HOST IDENTIFICATION HAS CHANGED
end
style H fill:#f59e0b,color:#000
style R fill:#2d7d46,color:#fff
style DB fill:#8b5cf6,color:#fff
style KH fill:#336791,color:#fff
%% fig: Alur TOFU di dalam check_server_key
%% desc: Handler membaca known_hosts, lalu mencatat key baru, memperbarui last_seen_at, atau menolak koneksi saat fingerprint berubah.Cabang pertama adalah inti Trust On First Use: handler menyusun KnownHost baru berisi first_seen_at dan last_seen_at yang sama, menyimpannya lewat save_known_host, lalu mengembalikan Ok(true). Cabang kedua memperbarui last_seen_at pada entri lama tanpa menyentuh fingerprint, juga dengan Ok(true). Cabang ketiga tidak mengembalikan Ok(false), melainkan Err(anyhow!...) sehingga handshake berhenti. src-tauri/src/ssh/mod.rs:71-8411 src-tauri/src/ssh/mod.rs:85-9112 src-tauri/src/ssh/mod.rs:92-9813
| Cabang | Kondisi match | Aksi ke known_hosts | Nilai balik |
|---|---|---|---|
| Key baru | None | save_known_host dengan first_seen_at = last_seen_at = waktu sekarang | Ok(true) |
| Key cocok | Some(known) if known.fingerprint == fingerprint | save_known_host dengan last_seen_at diperbarui | Ok(true) |
| Key berubah | Some(known) | tidak ada penulisan | Err berisi pesan REMOTE HOST IDENTIFICATION HAS CHANGED |
Ketiga cabang itu berbagi satu sumber data: get_known_host melakukan SELECT address, port, key_type, fingerprint, first_seen_at, last_seen_at FROM known_hosts WHERE address = ?1 AND port = ?2. src-tauri/src/db/mod.rs:843-86114
Bentuk data host yang dipercaya
Struct KnownHost memuat enam field yang seluruhnya diserialisasi: address, port, key_type, fingerprint, first_seen_at, dan last_seen_at. Tidak ada kolom tambahan untuk status atau alasan penolakan; satu baris mewakili satu pasangan alamat:port. src-tauri/src/db/models.rs:269-27715
| Kolom | Tipe SQL | Peran dalam verifikasi |
|---|---|---|
address | TEXT NOT NULL | bagian pertama PRIMARY KEY (address, port) dan kunci pencarian |
port | INTEGER NOT NULL | bagian kedua primary key; port berbeda diperlakukan sebagai host berbeda |
key_type | TEXT NOT NULL | algoritma key dari server_public_key.algorithm(), hanya untuk tampilan |
fingerprint | TEXT NOT NULL | satu-satunya nilai yang dibandingkan saat verifikasi |
first_seen_at | TEXT NOT NULL | waktu key pertama dipercaya, ikut dikutip di pesan penolakan |
last_seen_at | TEXT NOT NULL | diperbarui setiap kali fingerprint cocok, dipakai untuk urutan daftar |
Skema tabelnya dideklarasikan dengan PRIMARY KEY (address, port), sehingga satu host hanya punya satu key yang dipercaya pada satu waktu. src-tauri/src/db/mod.rs:112-12016 Penulisan memakai INSERT ... ON CONFLICT(address, port) DO UPDATE SET key_type=excluded.key_type, fingerprint=excluded.fingerprint, last_seen_at=excluded.last_seen_at; perhatikan bahwa first_seen_at sengaja tidak ikut di-update, jadi waktu kepercayaan pertama tetap terjaga walau last_seen_at terus bergerak. src-tauri/src/db/mod.rs:863-88217
Ketika fingerprint berubah
⚠️ Cabang mismatch mengembalikan
Err, sehinggaclient::connect_streamgagal dan koneksi tidak pernah sampai ke tahap autentikasi. Pesan yang dipakai menyebut address, port, fingerprint yang baru, fingerprint yang lama, danfirst_seen_atdari key lama, lalu menyarankan menghapus entri lama dari Known Hosts bila perubahan itu memang dipercaya. src-tauri/src/ssh/mod.rs:92-9813
Pada terminal, kegagalan ini muncul setelah progress step: 2 dengan nama host_key diumumkan, dan pemanggil melaporkannya lewat jalur error koneksi masing-masing, misalnya Connection to target failed: {e} untuk target dan SFTP SSH connection failed: {e} untuk SFTP. src-tauri/src/ssh/mod.rs:464-4814 src-tauri/src/sftp/mod.rs:67-826
Mereset kepercayaan satu host
Satu-satunya cara memperbaiki mismatch adalah menghapus barisnya, karena handler tidak punya jalur untuk menimpa fingerprint yang berbeda secara diam-diam. Penghapusan itu tersedia lewat command known_host_delete, yang meneruskan address dan port ke delete_known_host dan mengeksekusi DELETE FROM known_hosts WHERE address = ?1 AND port = ?2. Daftar host yang dipercaya dibaca lewat known_host_list yang memanggil list_known_hosts dan mengurutkan hasilnya dengan ORDER BY last_seen_at DESC. src-tauri/src/commands/mod.rs:1427-143918 src-tauri/src/db/mod.rs:884-89119 src-tauri/src/db/mod.rs:893-90920
Di frontend kedua command itu dibungkus sebagai listKnownHosts dan deleteKnownHost pada objek api. src/lib/api.ts:354-35721 Store useKnownHostsStore memanggil deleteKnownHost lalu langsung menyegarkan daftar, sehingga tabel di UI tidak menampilkan entri yang sudah dihapus. src/stores/useKnownHostsStore.ts:30-3322 Sebelum penghapusan dijalankan, tab Known Hosts menampilkan dialog konfirmasi berjudul "Reset Trusted Host Key" yang menjelaskan bahwa Termimus akan menerima key baru server pada koneksi berikutnya. src/components/settings/SettingsView.tsx:457-46723
Trust store tidak ikut ditimpa saat restore
Verifikasi TOFU hanya bermakna bila isi known_hosts tidak bisa diubah dari luar. Karena itu proses restore dan merge sengaja menolak mengganti fingerprint yang sudah dipercaya: entri baru dan entri yang fingerprint-nya identik tetap diimpor, sedangkan entri yang berbeda hanya dihitung sebagai known_hosts_conflicts dan dibiarkan di luar tabel. Aturan lengkapnya, termasuk efek opsi replace_all, dibahas di Backup, Sync Relay, dan API Server. src-tauri/src/db/mod.rs:1202-123324