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