From f30b1a519771ac49e2b03640e742fdb0a246bd81 Mon Sep 17 00:00:00 2001 From: salmonik Date: Thu, 29 Oct 2026 22:22:27 +0100 Subject: v2.4.0: key stored credentials by certificate fingerprint --- tests/storage.rs | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 5 deletions(-) (limited to 'tests') diff --git a/tests/storage.rs b/tests/storage.rs index b6e1369..19936c2 100644 --- a/tests/storage.rs +++ b/tests/storage.rs @@ -20,20 +20,123 @@ fn client_credentials_roundtrip() { let (_, conn) = tmp_db("creds"); client_database::init_db(&conn); - assert_eq!(client_database::get_credentials(&conn, "h1"), None); - client_database::save_credentials(&conn, "h1", "alice", "pw").unwrap(); + let identity = "5a15c712230831cb"; + assert_eq!(client_database::get_credentials(&conn, identity), None); + client_database::save_credentials(&conn, identity, "alice", "pw").unwrap(); assert_eq!( - client_database::get_credentials(&conn, "h1"), + client_database::get_credentials(&conn, identity), Some(("alice".to_string(), "pw".to_string())) ); // Overwrite works. - client_database::save_credentials(&conn, "h1", "alice", "pw2").unwrap(); + client_database::save_credentials(&conn, identity, "alice", "pw2").unwrap(); assert_eq!( - client_database::get_credentials(&conn, "h1"), + client_database::get_credentials(&conn, identity), Some(("alice".to_string(), "pw2".to_string())) ); } +#[test] +fn credentials_belong_to_the_certificate_not_the_address() { + let (_, conn) = tmp_db("creds-identity"); + client_database::init_db(&conn); + + // Two addresses for one server, one address for another. + client_database::save_host(&conn, "10.0.0.100:11525"); + client_database::save_host(&conn, "127.0.0.1:11525"); + client_database::save_host(&conn, "other.example:11525"); + client_database::save_fingerprint(&conn, "10.0.0.100:11525", "server-a").unwrap(); + client_database::save_fingerprint(&conn, "127.0.0.1:11525", "server-a").unwrap(); + client_database::save_fingerprint(&conn, "other.example:11525", "server-b").unwrap(); + + client_database::save_credentials(&conn, "server-a", "alice", "pw").unwrap(); + + assert_eq!( + client_database::get_credentials(&conn, "server-a"), + Some(("alice".to_string(), "pw".to_string())), + "one login covers every address with the same key" + ); + assert_eq!( + client_database::get_credentials(&conn, "server-b"), + None, + "a different key must not inherit the credentials" + ); +} + +#[test] +fn forgetting_one_address_keeps_the_shared_credentials() { + let (_, conn) = tmp_db("creds-forget"); + client_database::init_db(&conn); + + client_database::save_host(&conn, "10.0.0.100:11525"); + client_database::save_host(&conn, "127.0.0.1:11525"); + client_database::save_fingerprint(&conn, "10.0.0.100:11525", "server-a").unwrap(); + client_database::save_fingerprint(&conn, "127.0.0.1:11525", "server-a").unwrap(); + client_database::save_credentials(&conn, "server-a", "alice", "pw").unwrap(); + + let outcome = client_database::forget_host(&conn, "10.0.0.100:11525").unwrap(); + assert!(!outcome.credentials_removed); + assert_eq!(outcome.siblings, vec!["127.0.0.1:11525".to_string()]); + assert_eq!( + client_database::get_credentials(&conn, "server-a"), + Some(("alice".to_string(), "pw".to_string())), + "the server is still reachable, so the login is still useful" + ); + + let outcome = client_database::forget_host(&conn, "127.0.0.1:11525").unwrap(); + assert!(outcome.credentials_removed); + assert!(outcome.siblings.is_empty()); + assert_eq!(client_database::get_credentials(&conn, "server-a"), None); +} + +#[test] +fn legacy_host_keyed_credentials_are_rekeyed_by_fingerprint() { + let (_, conn) = tmp_db("creds-migrate"); + conn.execute_batch( + "CREATE TABLE hosts ( + host TEXT PRIMARY KEY, + last_used DATETIME DEFAULT CURRENT_TIMESTAMP, + fingerprint TEXT + ); + CREATE TABLE credentials ( + host TEXT PRIMARY KEY, + username TEXT NOT NULL, + password TEXT NOT NULL + ); + INSERT INTO hosts (host, last_used, fingerprint) + VALUES ('a:1', '2020-01-01 00:00:00', 'server-a'); + INSERT INTO hosts (host, last_used, fingerprint) + VALUES ('b:2', '2021-01-01 00:00:00', 'server-a'); + INSERT INTO hosts (host, last_used, fingerprint) + VALUES ('c:3', '2022-01-01 00:00:00', NULL); + INSERT INTO credentials VALUES ('a:1', 'alice', 'old'); + INSERT INTO credentials VALUES ('b:2', 'alice', 'recent'); + INSERT INTO credentials VALUES ('c:3', 'carol', 'untrusted');", + ) + .unwrap(); + + client_database::init_db(&conn); + + assert_eq!( + client_database::get_credentials(&conn, "server-a"), + Some(("alice".to_string(), "recent".to_string())), + "the most recently used address wins when legacy rows disagree" + ); + assert_eq!( + client_database::get_credentials(&conn, "carol"), + None, + "a host with no trusted key cannot be re-keyed" + ); + + // The old table is gone: the column set is the fingerprint schema. + let mut statement = conn.prepare("PRAGMA table_info(credentials)").unwrap(); + let columns: Vec = statement + .query_map([], |row| row.get(1)) + .unwrap() + .filter_map(|row| row.ok()) + .collect(); + assert_eq!(columns, vec!["fingerprint", "username", "password"]); +} + #[test] fn client_last_host_tracking() { let (_, conn) = tmp_db("hosts"); -- cgit v1.2.3