xref: /openbmc/qemu/qga/commands-posix-ssh.c (revision 7d87775f)
1  /*
2   * This work is licensed under the terms of the GNU GPL, version 2 or later.
3   * See the COPYING file in the top-level directory.
4   */
5 #include "qemu/osdep.h"
6 
7 #include <glib-unix.h>
8 #include <glib/gstdio.h>
9 #include <locale.h>
10 #include <pwd.h>
11 
12 #include "commands-common-ssh.h"
13 #include "qapi/error.h"
14 #include "qga-qapi-commands.h"
15 
16 #ifdef QGA_BUILD_UNIT_TEST
17 static struct passwd *
18 test_get_passwd_entry(const gchar *user_name, GError **error)
19 {
20     struct passwd *p;
21     int ret;
22 
23     if (!user_name || g_strcmp0(user_name, g_get_user_name())) {
24         g_set_error(error, G_UNIX_ERROR, 0, "Invalid user name");
25         return NULL;
26     }
27 
28     p = g_new0(struct passwd, 1);
29     p->pw_dir = (char *)g_get_home_dir();
30     p->pw_uid = geteuid();
31     p->pw_gid = getegid();
32 
33     ret = g_mkdir_with_parents(p->pw_dir, 0700);
34     g_assert(ret == 0);
35 
36     return p;
37 }
38 
39 #define g_unix_get_passwd_entry(username, err) \
40    test_get_passwd_entry(username, err)
41 #endif
42 
43 static struct passwd *
44 get_passwd_entry(const char *username, Error **errp)
45 {
46     g_autoptr(GError) err = NULL;
47     struct passwd *p;
48 
49     p = g_unix_get_passwd_entry(username, &err);
50     if (p == NULL) {
51         error_setg(errp, "failed to lookup user '%s': %s",
52                    username, err->message);
53         return NULL;
54     }
55 
56     return p;
57 }
58 
59 static bool
60 mkdir_for_user(const char *path, const struct passwd *p,
61                mode_t mode, Error **errp)
62 {
63     if (g_mkdir(path, mode) == -1) {
64         error_setg(errp, "failed to create directory '%s': %s",
65                    path, g_strerror(errno));
66         return false;
67     }
68 
69     if (chown(path, p->pw_uid, p->pw_gid) == -1) {
70         error_setg(errp, "failed to set ownership of directory '%s': %s",
71                    path, g_strerror(errno));
72         return false;
73     }
74 
75     if (chmod(path, mode) == -1) {
76         error_setg(errp, "failed to set permissions of directory '%s': %s",
77                    path, g_strerror(errno));
78         return false;
79     }
80 
81     return true;
82 }
83 
84 static bool
85 write_authkeys(const char *path, const GStrv keys,
86                const struct passwd *p, Error **errp)
87 {
88     g_autofree char *contents = NULL;
89     g_autoptr(GError) err = NULL;
90 
91     contents = g_strjoinv("\n", keys);
92     if (!g_file_set_contents(path, contents, -1, &err)) {
93         error_setg(errp, "failed to write to '%s': %s", path, err->message);
94         return false;
95     }
96 
97     if (chown(path, p->pw_uid, p->pw_gid) == -1) {
98         error_setg(errp, "failed to set ownership of directory '%s': %s",
99                    path, g_strerror(errno));
100         return false;
101     }
102 
103     if (chmod(path, 0600) == -1) {
104         error_setg(errp, "failed to set permissions of '%s': %s",
105                    path, g_strerror(errno));
106         return false;
107     }
108 
109     return true;
110 }
111 
112 void
113 qmp_guest_ssh_add_authorized_keys(const char *username, strList *keys,
114                                   bool has_reset, bool reset,
115                                   Error **errp)
116 {
117     g_autofree struct passwd *p = NULL;
118     g_autofree char *ssh_path = NULL;
119     g_autofree char *authkeys_path = NULL;
120     g_auto(GStrv) authkeys = NULL;
121     strList *k;
122     size_t nkeys, nauthkeys;
123 
124     reset = has_reset && reset;
125 
126     if (!check_openssh_pub_keys(keys, &nkeys, errp)) {
127         return;
128     }
129 
130     p = get_passwd_entry(username, errp);
131     if (p == NULL) {
132         return;
133     }
134 
135     ssh_path = g_build_filename(p->pw_dir, ".ssh", NULL);
136     authkeys_path = g_build_filename(ssh_path, "authorized_keys", NULL);
137 
138     if (!reset) {
139         authkeys = read_authkeys(authkeys_path, NULL);
140     }
141     if (authkeys == NULL) {
142         if (!g_file_test(ssh_path, G_FILE_TEST_IS_DIR) &&
143             !mkdir_for_user(ssh_path, p, 0700, errp)) {
144             return;
145         }
146     }
147 
148     nauthkeys = authkeys ? g_strv_length(authkeys) : 0;
149     authkeys = g_realloc_n(authkeys, nauthkeys + nkeys + 1, sizeof(char *));
150     memset(authkeys + nauthkeys, 0, (nkeys + 1) * sizeof(char *));
151 
152     for (k = keys; k != NULL; k = k->next) {
153         if (g_strv_contains((const gchar * const *)authkeys, k->value)) {
154             continue;
155         }
156         authkeys[nauthkeys++] = g_strdup(k->value);
157     }
158 
159     write_authkeys(authkeys_path, authkeys, p, errp);
160 }
161 
162 void
163 qmp_guest_ssh_remove_authorized_keys(const char *username, strList *keys,
164                                      Error **errp)
165 {
166     g_autofree struct passwd *p = NULL;
167     g_autofree char *authkeys_path = NULL;
168     g_autofree GStrv new_keys = NULL; /* do not own the strings */
169     g_auto(GStrv) authkeys = NULL;
170     GStrv a;
171     size_t nkeys = 0;
172 
173     if (!check_openssh_pub_keys(keys, NULL, errp)) {
174         return;
175     }
176 
177     p = get_passwd_entry(username, errp);
178     if (p == NULL) {
179         return;
180     }
181 
182     authkeys_path = g_build_filename(p->pw_dir, ".ssh",
183                                      "authorized_keys", NULL);
184     if (!g_file_test(authkeys_path, G_FILE_TEST_EXISTS)) {
185         return;
186     }
187     authkeys = read_authkeys(authkeys_path, errp);
188     if (authkeys == NULL) {
189         return;
190     }
191 
192     new_keys = g_new0(char *, g_strv_length(authkeys) + 1);
193     for (a = authkeys; *a != NULL; a++) {
194         strList *k;
195 
196         for (k = keys; k != NULL; k = k->next) {
197             if (g_str_equal(k->value, *a)) {
198                 break;
199             }
200         }
201         if (k != NULL) {
202             continue;
203         }
204 
205         new_keys[nkeys++] = *a;
206     }
207 
208     write_authkeys(authkeys_path, new_keys, p, errp);
209 }
210 
211 GuestAuthorizedKeys *
212 qmp_guest_ssh_get_authorized_keys(const char *username, Error **errp)
213 {
214     g_autofree struct passwd *p = NULL;
215     g_autofree char *authkeys_path = NULL;
216     g_auto(GStrv) authkeys = NULL;
217     g_autoptr(GuestAuthorizedKeys) ret = NULL;
218     int i;
219 
220     p = get_passwd_entry(username, errp);
221     if (p == NULL) {
222         return NULL;
223     }
224 
225     authkeys_path = g_build_filename(p->pw_dir, ".ssh",
226                                      "authorized_keys", NULL);
227     authkeys = read_authkeys(authkeys_path, errp);
228     if (authkeys == NULL) {
229         return NULL;
230     }
231 
232     ret = g_new0(GuestAuthorizedKeys, 1);
233     for (i = 0; authkeys[i] != NULL; i++) {
234         g_strstrip(authkeys[i]);
235         if (!authkeys[i][0] || authkeys[i][0] == '#') {
236             continue;
237         }
238 
239         QAPI_LIST_PREPEND(ret->keys, g_strdup(authkeys[i]));
240     }
241 
242     return g_steal_pointer(&ret);
243 }
244 
245 #ifdef QGA_BUILD_UNIT_TEST
246 static const strList test_key2 = {
247     .value = (char *)"algo key2 comments"
248 };
249 
250 static const strList test_key1_2 = {
251     .value = (char *)"algo key1 comments",
252     .next = (strList *)&test_key2,
253 };
254 
255 static char *
256 test_get_authorized_keys_path(void)
257 {
258     return g_build_filename(g_get_home_dir(), ".ssh", "authorized_keys", NULL);
259 }
260 
261 static void
262 test_authorized_keys_set(const char *contents)
263 {
264     g_autoptr(GError) err = NULL;
265     g_autofree char *path = NULL;
266     int ret;
267 
268     path = g_build_filename(g_get_home_dir(), ".ssh", NULL);
269     ret = g_mkdir_with_parents(path, 0700);
270     g_assert(ret == 0);
271     g_free(path);
272 
273     path = test_get_authorized_keys_path();
274     g_file_set_contents(path, contents, -1, &err);
275     g_assert(err == NULL);
276 }
277 
278 static void
279 test_authorized_keys_equal(const char *expected)
280 {
281     g_autoptr(GError) err = NULL;
282     g_autofree char *path = NULL;
283     g_autofree char *contents = NULL;
284 
285     path = test_get_authorized_keys_path();
286     g_file_get_contents(path, &contents, NULL, &err);
287     g_assert(err == NULL);
288 
289     g_assert(g_strcmp0(contents, expected) == 0);
290 }
291 
292 static void
293 test_invalid_user(void)
294 {
295     Error *err = NULL;
296 
297     qmp_guest_ssh_add_authorized_keys("", NULL, FALSE, FALSE, &err);
298     error_free_or_abort(&err);
299 
300     qmp_guest_ssh_remove_authorized_keys("", NULL, &err);
301     error_free_or_abort(&err);
302 }
303 
304 static void
305 test_invalid_key(void)
306 {
307     strList key = {
308         .value = (char *)"not a valid\nkey"
309     };
310     Error *err = NULL;
311 
312     qmp_guest_ssh_add_authorized_keys(g_get_user_name(), &key,
313                                       FALSE, FALSE, &err);
314     error_free_or_abort(&err);
315 
316     qmp_guest_ssh_remove_authorized_keys(g_get_user_name(), &key, &err);
317     error_free_or_abort(&err);
318 }
319 
320 static void
321 test_add_keys(void)
322 {
323     Error *err = NULL;
324 
325     qmp_guest_ssh_add_authorized_keys(g_get_user_name(),
326                                       (strList *)&test_key2,
327                                       FALSE, FALSE,
328                                       &err);
329     g_assert(err == NULL);
330 
331     test_authorized_keys_equal("algo key2 comments");
332 
333     qmp_guest_ssh_add_authorized_keys(g_get_user_name(),
334                                       (strList *)&test_key1_2,
335                                       FALSE, FALSE,
336                                       &err);
337     g_assert(err == NULL);
338 
339     /*  key2 came first, and shouldn't be duplicated */
340     test_authorized_keys_equal("algo key2 comments\n"
341                                "algo key1 comments");
342 }
343 
344 static void
345 test_add_reset_keys(void)
346 {
347     Error *err = NULL;
348 
349     qmp_guest_ssh_add_authorized_keys(g_get_user_name(),
350                                       (strList *)&test_key1_2,
351                                       FALSE, FALSE,
352                                       &err);
353     g_assert(err == NULL);
354 
355     /* reset with key2 only */
356     test_authorized_keys_equal("algo key1 comments\n"
357                                "algo key2 comments");
358 
359     qmp_guest_ssh_add_authorized_keys(g_get_user_name(),
360                                       (strList *)&test_key2,
361                                       TRUE, TRUE,
362                                       &err);
363     g_assert(err == NULL);
364 
365     test_authorized_keys_equal("algo key2 comments");
366 
367     /* empty should clear file */
368     qmp_guest_ssh_add_authorized_keys(g_get_user_name(),
369                                       (strList *)NULL,
370                                       TRUE, TRUE,
371                                       &err);
372     g_assert(err == NULL);
373 
374     test_authorized_keys_equal("");
375 }
376 
377 static void
378 test_remove_keys(void)
379 {
380     Error *err = NULL;
381     static const char *authkeys =
382         "algo key1 comments\n"
383         /* originally duplicated */
384         "algo key1 comments\n"
385         "# a commented line\n"
386         "algo some-key another\n";
387 
388     test_authorized_keys_set(authkeys);
389     qmp_guest_ssh_remove_authorized_keys(g_get_user_name(),
390                                          (strList *)&test_key2, &err);
391     g_assert(err == NULL);
392     test_authorized_keys_equal(authkeys);
393 
394     qmp_guest_ssh_remove_authorized_keys(g_get_user_name(),
395                                          (strList *)&test_key1_2, &err);
396     g_assert(err == NULL);
397     test_authorized_keys_equal("# a commented line\n"
398                                "algo some-key another\n");
399 }
400 
401 static void
402 test_get_keys(void)
403 {
404     Error *err = NULL;
405     static const char *authkeys =
406         "algo key1 comments\n"
407         "# a commented line\n"
408         "algo some-key another\n";
409     g_autoptr(GuestAuthorizedKeys) ret = NULL;
410     strList *k;
411     size_t len = 0;
412 
413     test_authorized_keys_set(authkeys);
414 
415     ret = qmp_guest_ssh_get_authorized_keys(g_get_user_name(), &err);
416     g_assert(err == NULL);
417 
418     for (len = 0, k = ret->keys; k != NULL; k = k->next) {
419         g_assert(g_str_has_prefix(k->value, "algo "));
420         len++;
421     }
422 
423     g_assert(len == 2);
424 }
425 
426 int main(int argc, char *argv[])
427 {
428     setlocale(LC_ALL, "");
429 
430     g_test_init(&argc, &argv, G_TEST_OPTION_ISOLATE_DIRS, NULL);
431 
432     g_test_add_func("/qga/ssh/invalid_user", test_invalid_user);
433     g_test_add_func("/qga/ssh/invalid_key", test_invalid_key);
434     g_test_add_func("/qga/ssh/add_keys", test_add_keys);
435     g_test_add_func("/qga/ssh/add_reset_keys", test_add_reset_keys);
436     g_test_add_func("/qga/ssh/remove_keys", test_remove_keys);
437     g_test_add_func("/qga/ssh/get_keys", test_get_keys);
438 
439     return g_test_run();
440 }
441 #endif /* BUILD_UNIT_TEST */
442