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