xref: /openbmc/qemu/qga/commands-posix-ssh.c (revision 86b7c551)
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_qemu(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_qemu(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 #if GLIB_CHECK_VERSION(2, 60, 0)
247 static const strList test_key2 = {
248     .value = (char *)"algo key2 comments"
249 };
250 
251 static const strList test_key1_2 = {
252     .value = (char *)"algo key1 comments",
253     .next = (strList *)&test_key2,
254 };
255 
256 static char *
257 test_get_authorized_keys_path(void)
258 {
259     return g_build_filename(g_get_home_dir(), ".ssh", "authorized_keys", NULL);
260 }
261 
262 static void
263 test_authorized_keys_set(const char *contents)
264 {
265     g_autoptr(GError) err = NULL;
266     g_autofree char *path = NULL;
267     int ret;
268 
269     path = g_build_filename(g_get_home_dir(), ".ssh", NULL);
270     ret = g_mkdir_with_parents(path, 0700);
271     g_assert(ret == 0);
272     g_free(path);
273 
274     path = test_get_authorized_keys_path();
275     g_file_set_contents(path, contents, -1, &err);
276     g_assert(err == NULL);
277 }
278 
279 static void
280 test_authorized_keys_equal(const char *expected)
281 {
282     g_autoptr(GError) err = NULL;
283     g_autofree char *path = NULL;
284     g_autofree char *contents = NULL;
285 
286     path = test_get_authorized_keys_path();
287     g_file_get_contents(path, &contents, NULL, &err);
288     g_assert(err == NULL);
289 
290     g_assert(g_strcmp0(contents, expected) == 0);
291 }
292 
293 static void
294 test_invalid_user(void)
295 {
296     Error *err = NULL;
297 
298     qmp_guest_ssh_add_authorized_keys("", NULL, FALSE, FALSE, &err);
299     error_free_or_abort(&err);
300 
301     qmp_guest_ssh_remove_authorized_keys("", NULL, &err);
302     error_free_or_abort(&err);
303 }
304 
305 static void
306 test_invalid_key(void)
307 {
308     strList key = {
309         .value = (char *)"not a valid\nkey"
310     };
311     Error *err = NULL;
312 
313     qmp_guest_ssh_add_authorized_keys(g_get_user_name(), &key,
314                                       FALSE, FALSE, &err);
315     error_free_or_abort(&err);
316 
317     qmp_guest_ssh_remove_authorized_keys(g_get_user_name(), &key, &err);
318     error_free_or_abort(&err);
319 }
320 
321 static void
322 test_add_keys(void)
323 {
324     Error *err = NULL;
325 
326     qmp_guest_ssh_add_authorized_keys(g_get_user_name(),
327                                       (strList *)&test_key2,
328                                       FALSE, FALSE,
329                                       &err);
330     g_assert(err == NULL);
331 
332     test_authorized_keys_equal("algo key2 comments");
333 
334     qmp_guest_ssh_add_authorized_keys(g_get_user_name(),
335                                       (strList *)&test_key1_2,
336                                       FALSE, FALSE,
337                                       &err);
338     g_assert(err == NULL);
339 
340     /*  key2 came first, and shouldn't be duplicated */
341     test_authorized_keys_equal("algo key2 comments\n"
342                                "algo key1 comments");
343 }
344 
345 static void
346 test_add_reset_keys(void)
347 {
348     Error *err = NULL;
349 
350     qmp_guest_ssh_add_authorized_keys(g_get_user_name(),
351                                       (strList *)&test_key1_2,
352                                       FALSE, FALSE,
353                                       &err);
354     g_assert(err == NULL);
355 
356     /* reset with key2 only */
357     test_authorized_keys_equal("algo key1 comments\n"
358                                "algo key2 comments");
359 
360     qmp_guest_ssh_add_authorized_keys(g_get_user_name(),
361                                       (strList *)&test_key2,
362                                       TRUE, TRUE,
363                                       &err);
364     g_assert(err == NULL);
365 
366     test_authorized_keys_equal("algo key2 comments");
367 
368     /* empty should clear file */
369     qmp_guest_ssh_add_authorized_keys(g_get_user_name(),
370                                       (strList *)NULL,
371                                       TRUE, TRUE,
372                                       &err);
373     g_assert(err == NULL);
374 
375     test_authorized_keys_equal("");
376 }
377 
378 static void
379 test_remove_keys(void)
380 {
381     Error *err = NULL;
382     static const char *authkeys =
383         "algo key1 comments\n"
384         /* originally duplicated */
385         "algo key1 comments\n"
386         "# a commented line\n"
387         "algo some-key another\n";
388 
389     test_authorized_keys_set(authkeys);
390     qmp_guest_ssh_remove_authorized_keys(g_get_user_name(),
391                                          (strList *)&test_key2, &err);
392     g_assert(err == NULL);
393     test_authorized_keys_equal(authkeys);
394 
395     qmp_guest_ssh_remove_authorized_keys(g_get_user_name(),
396                                          (strList *)&test_key1_2, &err);
397     g_assert(err == NULL);
398     test_authorized_keys_equal("# a commented line\n"
399                                "algo some-key another\n");
400 }
401 
402 static void
403 test_get_keys(void)
404 {
405     Error *err = NULL;
406     static const char *authkeys =
407         "algo key1 comments\n"
408         "# a commented line\n"
409         "algo some-key another\n";
410     g_autoptr(GuestAuthorizedKeys) ret = NULL;
411     strList *k;
412     size_t len = 0;
413 
414     test_authorized_keys_set(authkeys);
415 
416     ret = qmp_guest_ssh_get_authorized_keys(g_get_user_name(), &err);
417     g_assert(err == NULL);
418 
419     for (len = 0, k = ret->keys; k != NULL; k = k->next) {
420         g_assert(g_str_has_prefix(k->value, "algo "));
421         len++;
422     }
423 
424     g_assert(len == 2);
425 }
426 
427 int main(int argc, char *argv[])
428 {
429     setlocale(LC_ALL, "");
430 
431     g_test_init(&argc, &argv, G_TEST_OPTION_ISOLATE_DIRS, NULL);
432 
433     g_test_add_func("/qga/ssh/invalid_user", test_invalid_user);
434     g_test_add_func("/qga/ssh/invalid_key", test_invalid_key);
435     g_test_add_func("/qga/ssh/add_keys", test_add_keys);
436     g_test_add_func("/qga/ssh/add_reset_keys", test_add_reset_keys);
437     g_test_add_func("/qga/ssh/remove_keys", test_remove_keys);
438     g_test_add_func("/qga/ssh/get_keys", test_get_keys);
439 
440     return g_test_run();
441 }
442 #else
443 int main(int argc, char *argv[])
444 {
445     g_test_message("test skipped, needs glib >= 2.60");
446     return 0;
447 }
448 #endif /* GLIB_2_60 */
449 #endif /* BUILD_UNIT_TEST */
450