diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-24 15:27:06 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-24 15:27:06 +0200 |
| commit | 8218fd584e4053848738a310258893e14c4ee10e (patch) | |
| tree | 2c92997ba0b6aa260fe72c88190f5f8b590457cb /src/bin/client/database.rs | |
| parent | TLS transport and games framework on client and server (diff) | |
| download | zagan-bet-8218fd584e4053848738a310258893e14c4ee10e.tar.gz zagan-bet-8218fd584e4053848738a310258893e14c4ee10e.zip | |
TOFU host trust: SPKI fingerprint pinning, tests, forget-host
Diffstat (limited to 'src/bin/client/database.rs')
| -rw-r--r-- | src/bin/client/database.rs | 46 |
1 files changed, 41 insertions, 5 deletions
diff --git a/src/bin/client/database.rs b/src/bin/client/database.rs index a274f31..55051fb 100644 --- a/src/bin/client/database.rs +++ b/src/bin/client/database.rs | |||
| @@ -1,15 +1,17 @@ | |||
| 1 | use rusqlite::Connection; | 1 | use rusqlite::Connection; |
| 2 | 2 | ||
| 3 | pub fn init_db() -> Connection { | 3 | pub fn init_db(conn: &Connection) { |
| 4 | let conn = Connection::open("client.db").unwrap(); | ||
| 5 | conn.execute( | 4 | conn.execute( |
| 6 | "CREATE TABLE IF NOT EXISTS hosts ( | 5 | "CREATE TABLE IF NOT EXISTS hosts ( |
| 7 | host TEXT PRIMARY KEY, | 6 | host TEXT PRIMARY KEY, |
| 8 | last_used DATETIME DEFAULT CURRENT_TIMESTAMP | 7 | last_used DATETIME DEFAULT CURRENT_TIMESTAMP, |
| 8 | fingerprint TEXT | ||
| 9 | )", | 9 | )", |
| 10 | [], | 10 | [], |
| 11 | ) | 11 | ) |
| 12 | .unwrap(); | 12 | .unwrap(); |
| 13 | // Migration for databases created before TOFU trust. | ||
| 14 | let _ = conn.execute("ALTER TABLE hosts ADD COLUMN fingerprint TEXT", []); | ||
| 13 | conn.execute( | 15 | conn.execute( |
| 14 | "CREATE TABLE IF NOT EXISTS credentials ( | 16 | "CREATE TABLE IF NOT EXISTS credentials ( |
| 15 | host TEXT PRIMARY KEY, | 17 | host TEXT PRIMARY KEY, |
| @@ -19,7 +21,6 @@ pub fn init_db() -> Connection { | |||
| 19 | [], | 21 | [], |
| 20 | ) | 22 | ) |
| 21 | .unwrap(); | 23 | .unwrap(); |
| 22 | conn | ||
| 23 | } | 24 | } |
| 24 | 25 | ||
| 25 | pub fn get_last_host(conn: &Connection) -> Option<String> { | 26 | pub fn get_last_host(conn: &Connection) -> Option<String> { |
| @@ -36,7 +37,8 @@ pub fn get_last_host(conn: &Connection) -> Option<String> { | |||
| 36 | 37 | ||
| 37 | pub fn save_host(conn: &Connection, host: &str) { | 38 | pub fn save_host(conn: &Connection, host: &str) { |
| 38 | conn.execute( | 39 | conn.execute( |
| 39 | "INSERT OR REPLACE INTO hosts (host, last_used) VALUES (?1, CURRENT_TIMESTAMP)", | 40 | "INSERT INTO hosts (host) VALUES (?1) |
| 41 | ON CONFLICT(host) DO UPDATE SET last_used = CURRENT_TIMESTAMP", | ||
| 40 | [host], | 42 | [host], |
| 41 | ) | 43 | ) |
| 42 | .unwrap(); | 44 | .unwrap(); |
| @@ -68,3 +70,37 @@ pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String) | |||
| 68 | None | 70 | None |
| 69 | } | 71 | } |
| 70 | } | 72 | } |
| 73 | |||
| 74 | /// SHA-256 SPKI fingerprint of a host's certificate, if trusted. | ||
| 75 | pub fn get_fingerprint(conn: &Connection, host: &str) -> Option<String> { | ||
| 76 | let mut stmt = conn | ||
| 77 | .prepare("SELECT fingerprint FROM hosts WHERE host = ?1") | ||
| 78 | .ok()?; | ||
| 79 | let mut rows = stmt.query([host]).ok()?; | ||
| 80 | match rows.next() { | ||
| 81 | Ok(Some(row)) => row.get(0).ok(), | ||
| 82 | _ => None, | ||
| 83 | } | ||
| 84 | } | ||
| 85 | |||
| 86 | pub fn save_fingerprint( | ||
| 87 | conn: &Connection, | ||
| 88 | host: &str, | ||
| 89 | fingerprint: &str, | ||
| 90 | ) -> Result<(), rusqlite::Error> { | ||
| 91 | // Hosts may not exist yet (fingerprint can be saved before first | ||
| 92 | // successful connect completes save_host). | ||
| 93 | conn.execute("INSERT OR IGNORE INTO hosts (host) VALUES (?1)", [host])?; | ||
| 94 | conn.execute( | ||
| 95 | "UPDATE hosts SET fingerprint = ?2 WHERE host = ?1", | ||
| 96 | (host, fingerprint), | ||
| 97 | )?; | ||
| 98 | Ok(()) | ||
| 99 | } | ||
| 100 | |||
| 101 | /// Remove all trust and credentials for a host ("forget this host"). | ||
| 102 | pub fn forget_host(conn: &Connection, host: &str) -> Result<(), rusqlite::Error> { | ||
| 103 | conn.execute("DELETE FROM hosts WHERE host = ?1", [host])?; | ||
| 104 | conn.execute("DELETE FROM credentials WHERE host = ?1", [host])?; | ||
| 105 | Ok(()) | ||
| 106 | } | ||
