Skip to main content

max / makenotwork

5.4 KB · 169 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 crate::harness::seed_user;
11 use makenotwork::db::ssh_keys;
12
13 #[tokio::test]
14 async fn add_then_lookup_resolves_the_owner() {
15 let db = TestDb::new().await;
16 let user = seed_user(&db.pool, "ssh_lookup").await;
17
18 ssh_keys::add_key(&db.pool, user, "ssh-ed25519 AAAA", "SHA256:aaa", "laptop")
19 .await
20 .unwrap();
21
22 let found = ssh_keys::lookup_user_by_fingerprint(&db.pool, "SHA256:aaa")
23 .await
24 .unwrap()
25 .expect("known fingerprint resolves");
26 assert_eq!(found.user_id, user);
27 // The CLI takes its display currency from this one lookup and has no other
28 // source for it, so dropping the column here renders every amount in the
29 // TUI as USD whoever the creator is.
30 assert_eq!(
31 found.settlement_currency,
32 makenotwork::currency::SettlementCurrency::Usd,
33 "a seeded user settles in the column default"
34 );
35
36 // An unknown fingerprint authenticates nobody.
37 assert!(
38 ssh_keys::lookup_user_by_fingerprint(&db.pool, "SHA256:unknown")
39 .await
40 .unwrap()
41 .is_none()
42 );
43 }
44
45 #[tokio::test]
46 async fn duplicate_fingerprint_for_same_user_is_rejected() {
47 let db = TestDb::new().await;
48 let user = seed_user(&db.pool, "ssh_dup").await;
49
50 ssh_keys::add_key(&db.pool, user, "ssh-ed25519 AAAA", "SHA256:dup", "one")
51 .await
52 .unwrap();
53 // UNIQUE (user_id, fingerprint), re-registering the same key fails.
54 let dup = ssh_keys::add_key(&db.pool, user, "ssh-ed25519 AAAA", "SHA256:dup", "two").await;
55 assert!(
56 dup.is_err(),
57 "the same fingerprint cannot be registered twice by one user"
58 );
59 }
60
61 /// A fingerprint maps to exactly ONE identity, globally. Migration 161 added a
62 /// global `UNIQUE (fingerprint)` on top of the original `(user_id, fingerprint)`
63 /// specifically so one account can't register a public key already tied to
64 /// another account and thereby authenticate as it (Run 15 Auth C1). This guards
65 /// that fix: a second user registering the same fingerprint is rejected, so
66 /// `lookup_user_by_fingerprint` can never be ambiguous.
67 #[tokio::test]
68 async fn fingerprint_is_globally_unique_across_users() {
69 let db = TestDb::new().await;
70 let first = seed_user(&db.pool, "ssh_first").await;
71 let second = seed_user(&db.pool, "ssh_second").await;
72
73 ssh_keys::add_key(
74 &db.pool,
75 first,
76 "ssh-ed25519 AAAA",
77 "SHA256:shared",
78 "first",
79 )
80 .await
81 .unwrap();
82 // A different account cannot claim the same fingerprint.
83 let stolen = ssh_keys::add_key(
84 &db.pool,
85 second,
86 "ssh-ed25519 AAAA",
87 "SHA256:shared",
88 "second",
89 )
90 .await;
91 assert!(
92 stolen.is_err(),
93 "a fingerprint already registered to one account must not be registrable by another"
94 );
95
96 // The lookup remains unambiguous, it resolves to the sole registrant.
97 let resolved = ssh_keys::lookup_user_by_fingerprint(&db.pool, "SHA256:shared")
98 .await
99 .unwrap()
100 .expect("fingerprint resolves");
101 assert_eq!(resolved.user_id, first);
102 }
103
104 /// IDOR guard: `delete_key` scopes on `(id, user_id)`, so one user cannot delete
105 /// another's key by id.
106 #[tokio::test]
107 async fn delete_by_id_is_scoped_to_the_owner() {
108 let db = TestDb::new().await;
109 let owner = seed_user(&db.pool, "ssh_owner").await;
110 let attacker = seed_user(&db.pool, "ssh_attacker").await;
111 let key = ssh_keys::add_key(&db.pool, owner, "ssh-ed25519 AAAA", "SHA256:own", "k")
112 .await
113 .unwrap();
114
115 assert!(
116 !ssh_keys::delete_key(&db.pool, key.id, attacker)
117 .await
118 .unwrap(),
119 "a non-owner delete-by-id must return false"
120 );
121 assert_eq!(
122 ssh_keys::list_keys_by_user(&db.pool, owner)
123 .await
124 .unwrap()
125 .len(),
126 1,
127 "the key survives"
128 );
129
130 assert!(ssh_keys::delete_key(&db.pool, key.id, owner).await.unwrap());
131 assert!(
132 ssh_keys::list_keys_by_user(&db.pool, owner)
133 .await
134 .unwrap()
135 .is_empty()
136 );
137 }
138
139 #[tokio::test]
140 async fn delete_by_fingerprint_is_scoped_to_the_owner() {
141 let db = TestDb::new().await;
142 let owner = seed_user(&db.pool, "ssh_fp_owner").await;
143 let attacker = seed_user(&db.pool, "ssh_fp_attacker").await;
144 ssh_keys::add_key(&db.pool, owner, "ssh-ed25519 AAAA", "SHA256:fp", "k")
145 .await
146 .unwrap();
147
148 // The attacker knows the fingerprint but not the owner scope: no-op.
149 assert!(
150 !ssh_keys::delete_key_by_fingerprint(&db.pool, attacker, "SHA256:fp")
151 .await
152 .unwrap(),
153 "a non-owner delete-by-fingerprint must return false"
154 );
155 assert_eq!(
156 ssh_keys::list_keys_by_user(&db.pool, owner)
157 .await
158 .unwrap()
159 .len(),
160 1
161 );
162
163 assert!(
164 ssh_keys::delete_key_by_fingerprint(&db.pool, owner, "SHA256:fp")
165 .await
166 .unwrap()
167 );
168 }
169