Skip to main content

max / makenotwork

5.5 KB · 173 lines History Blame Raw
1 //! DB-layer contract tests for `db::ssh_keys`, git-over-SSH key CRUD + lookup.
2 //!
3 //! Audit Run 16 flagged `db/ssh_keys.rs:98` (`lookup_user_by_fingerprint`) and
4 //! the module's thin coverage. These pin the ownership-scoped deletes (an IDOR
5 //! guard on the git surface), the per-user duplicate rejection, and the global
6 //! fingerprint uniqueness (migration 161) that keeps `lookup_user_by_fingerprint`
7 //! unambiguous, one account can't register another's key fingerprint.
8
9 use crate::harness::db::TestDb;
10 use makenotwork::db::{UserId, ssh_keys};
11
12 async fn seed_user(pool: &sqlx::PgPool, username: &str) -> UserId {
13 let hash = makenotwork::auth::hash_password("password123").expect("hash");
14 sqlx::query_scalar::<_, UserId>(
15 "INSERT INTO users (username, email, password_hash, email_verified)
16 VALUES ($1, $2, $3, true) RETURNING id",
17 )
18 .bind(username)
19 .bind(format!("{username}@test.com"))
20 .bind(&hash)
21 .fetch_one(pool)
22 .await
23 .expect("seed user")
24 }
25
26 #[tokio::test]
27 async fn add_then_lookup_resolves_the_owner() {
28 let db = TestDb::new().await;
29 let user = seed_user(&db.pool, "ssh_lookup").await;
30
31 ssh_keys::add_key(&db.pool, user, "ssh-ed25519 AAAA", "SHA256:aaa", "laptop")
32 .await
33 .unwrap();
34
35 let found = ssh_keys::lookup_user_by_fingerprint(&db.pool, "SHA256:aaa")
36 .await
37 .unwrap();
38 assert_eq!(found.expect("known fingerprint resolves").user_id, user);
39
40 // An unknown fingerprint authenticates nobody.
41 assert!(
42 ssh_keys::lookup_user_by_fingerprint(&db.pool, "SHA256:unknown")
43 .await
44 .unwrap()
45 .is_none()
46 );
47 }
48
49 #[tokio::test]
50 async fn duplicate_fingerprint_for_same_user_is_rejected() {
51 let db = TestDb::new().await;
52 let user = seed_user(&db.pool, "ssh_dup").await;
53
54 ssh_keys::add_key(&db.pool, user, "ssh-ed25519 AAAA", "SHA256:dup", "one")
55 .await
56 .unwrap();
57 // UNIQUE (user_id, fingerprint), re-registering the same key fails.
58 let dup = ssh_keys::add_key(&db.pool, user, "ssh-ed25519 AAAA", "SHA256:dup", "two").await;
59 assert!(
60 dup.is_err(),
61 "the same fingerprint cannot be registered twice by one user"
62 );
63 }
64
65 /// A fingerprint maps to exactly ONE identity, globally. Migration 161 added a
66 /// global `UNIQUE (fingerprint)` on top of the original `(user_id, fingerprint)`
67 /// specifically so one account can't register a public key already tied to
68 /// another account and thereby authenticate as it (Run 15 Auth C1). This guards
69 /// that fix: a second user registering the same fingerprint is rejected, so
70 /// `lookup_user_by_fingerprint` can never be ambiguous.
71 #[tokio::test]
72 async fn fingerprint_is_globally_unique_across_users() {
73 let db = TestDb::new().await;
74 let first = seed_user(&db.pool, "ssh_first").await;
75 let second = seed_user(&db.pool, "ssh_second").await;
76
77 ssh_keys::add_key(
78 &db.pool,
79 first,
80 "ssh-ed25519 AAAA",
81 "SHA256:shared",
82 "first",
83 )
84 .await
85 .unwrap();
86 // A different account cannot claim the same fingerprint.
87 let stolen = ssh_keys::add_key(
88 &db.pool,
89 second,
90 "ssh-ed25519 AAAA",
91 "SHA256:shared",
92 "second",
93 )
94 .await;
95 assert!(
96 stolen.is_err(),
97 "a fingerprint already registered to one account must not be registrable by another"
98 );
99
100 // The lookup remains unambiguous, it resolves to the sole registrant.
101 let resolved = ssh_keys::lookup_user_by_fingerprint(&db.pool, "SHA256:shared")
102 .await
103 .unwrap()
104 .expect("fingerprint resolves");
105 assert_eq!(resolved.user_id, first);
106 }
107
108 /// IDOR guard: `delete_key` scopes on `(id, user_id)`, so one user cannot delete
109 /// another's key by id.
110 #[tokio::test]
111 async fn delete_by_id_is_scoped_to_the_owner() {
112 let db = TestDb::new().await;
113 let owner = seed_user(&db.pool, "ssh_owner").await;
114 let attacker = seed_user(&db.pool, "ssh_attacker").await;
115 let key = ssh_keys::add_key(&db.pool, owner, "ssh-ed25519 AAAA", "SHA256:own", "k")
116 .await
117 .unwrap();
118
119 assert!(
120 !ssh_keys::delete_key(&db.pool, key.id, attacker)
121 .await
122 .unwrap(),
123 "a non-owner delete-by-id must return false"
124 );
125 assert_eq!(
126 ssh_keys::list_keys_by_user(&db.pool, owner)
127 .await
128 .unwrap()
129 .len(),
130 1,
131 "the key survives"
132 );
133
134 assert!(ssh_keys::delete_key(&db.pool, key.id, owner).await.unwrap());
135 assert!(
136 ssh_keys::list_keys_by_user(&db.pool, owner)
137 .await
138 .unwrap()
139 .is_empty()
140 );
141 }
142
143 #[tokio::test]
144 async fn delete_by_fingerprint_is_scoped_to_the_owner() {
145 let db = TestDb::new().await;
146 let owner = seed_user(&db.pool, "ssh_fp_owner").await;
147 let attacker = seed_user(&db.pool, "ssh_fp_attacker").await;
148 ssh_keys::add_key(&db.pool, owner, "ssh-ed25519 AAAA", "SHA256:fp", "k")
149 .await
150 .unwrap();
151
152 // The attacker knows the fingerprint but not the owner scope: no-op.
153 assert!(
154 !ssh_keys::delete_key_by_fingerprint(&db.pool, attacker, "SHA256:fp")
155 .await
156 .unwrap(),
157 "a non-owner delete-by-fingerprint must return false"
158 );
159 assert_eq!(
160 ssh_keys::list_keys_by_user(&db.pool, owner)
161 .await
162 .unwrap()
163 .len(),
164 1
165 );
166
167 assert!(
168 ssh_keys::delete_key_by_fingerprint(&db.pool, owner, "SHA256:fp")
169 .await
170 .unwrap()
171 );
172 }
173