1 /* 2 * QEMU Guest Agent win32-specific command implementations for SSH keys. 3 * The implementation is opinionated and expects the SSH implementation to 4 * be OpenSSH. 5 * 6 * Copyright Schweitzer Engineering Laboratories. 2024 7 * 8 * Authors: 9 * Aidan Leuck <aidan_leuck@selinc.com> 10 * 11 * This work is licensed under the terms of the GNU GPL, version 2 or later. 12 * See the COPYING file in the top-level directory. 13 */ 14 15 #include "qemu/osdep.h" 16 #include <aclapi.h> 17 #include <qga-qapi-types.h> 18 19 #include "commands-common-ssh.h" 20 #include "commands-windows-ssh.h" 21 #include "guest-agent-core.h" 22 #include "limits.h" 23 #include "lmaccess.h" 24 #include "lmapibuf.h" 25 #include "lmerr.h" 26 #include "qapi/error.h" 27 28 #include "qga-qapi-commands.h" 29 #include "sddl.h" 30 #include "shlobj.h" 31 #include "userenv.h" 32 33 #define AUTHORIZED_KEY_FILE "authorized_keys" 34 #define AUTHORIZED_KEY_FILE_ADMIN "administrators_authorized_keys" 35 #define LOCAL_SYSTEM_SID "S-1-5-18" 36 #define ADMIN_SID "S-1-5-32-544" 37 38 /* 39 * Frees userInfo structure. This implements the g_auto cleanup 40 * for the structure. 41 */ 42 void free_userInfo(PWindowsUserInfo info) 43 { 44 g_free(info->sshDirectory); 45 g_free(info->authorizedKeyFile); 46 LocalFree(info->SSID); 47 g_free(info->username); 48 g_free(info); 49 } 50 51 /* 52 * Gets the admin SSH folder for OpenSSH. OpenSSH does not store 53 * the authorized_key file in the users home directory for security reasons and 54 * instead stores it at %PROGRAMDATA%/ssh. This function returns the path to 55 * that directory on the users machine 56 * 57 * parameters: 58 * errp -> error structure to set when an error occurs 59 * returns: The path to the ssh folder in %PROGRAMDATA% or NULL if an error 60 * occurred. 61 */ 62 static char *get_admin_ssh_folder(Error **errp) 63 { 64 /* Allocate memory for the program data path */ 65 g_autofree char *programDataPath = NULL; 66 char *authkeys_path = NULL; 67 PWSTR pgDataW = NULL; 68 g_autoptr(GError) gerr = NULL; 69 70 /* Get the KnownFolderPath on the machine. */ 71 HRESULT folderResult = 72 SHGetKnownFolderPath(&FOLDERID_ProgramData, 0, NULL, &pgDataW); 73 if (folderResult != S_OK) { 74 error_setg(errp, "Failed to retrieve ProgramData folder"); 75 return NULL; 76 } 77 78 /* Convert from a wide string back to a standard character string. */ 79 programDataPath = g_utf16_to_utf8(pgDataW, -1, NULL, NULL, &gerr); 80 CoTaskMemFree(pgDataW); 81 if (!programDataPath) { 82 error_setg(errp, 83 "Failed converting ProgramData folder path to UTF-16 %s", 84 gerr->message); 85 return NULL; 86 } 87 88 /* Build the path to the file. */ 89 authkeys_path = g_build_filename(programDataPath, "ssh", NULL); 90 return authkeys_path; 91 } 92 93 /* 94 * Gets the path to the SSH folder for the specified user. If the user is an 95 * admin it returns the ssh folder located at %PROGRAMDATA%/ssh. If the user is 96 * not an admin it returns %USERPROFILE%/.ssh 97 * 98 * parameters: 99 * username -> Username to get the SSH folder for 100 * isAdmin -> Whether the user is an admin or not 101 * errp -> Error structure to set any errors that occur. 102 * returns: path to the ssh folder as a string. 103 */ 104 static char *get_ssh_folder(const char *username, const bool isAdmin, 105 Error **errp) 106 { 107 DWORD maxSize = MAX_PATH; 108 g_autofree char *profilesDir = g_new0(char, maxSize); 109 110 if (isAdmin) { 111 return get_admin_ssh_folder(errp); 112 } 113 114 /* If not an Admin the SSH key is in the user directory. */ 115 /* Get the user profile directory on the machine. */ 116 BOOL ret = GetProfilesDirectory(profilesDir, &maxSize); 117 if (!ret) { 118 error_setg_win32(errp, GetLastError(), 119 "failed to retrieve profiles directory"); 120 return NULL; 121 } 122 123 /* Builds the filename */ 124 return g_build_filename(profilesDir, username, ".ssh", NULL); 125 } 126 127 /* 128 * Creates an entry for the user so they can access the ssh folder in their 129 * userprofile. 130 * 131 * parameters: 132 * userInfo -> Information about the current user 133 * pACL -> Pointer to an ACL structure 134 * errp -> Error structure to set any errors that occur 135 * returns -> 1 on success, 0 otherwise 136 */ 137 static bool create_acl_user(PWindowsUserInfo userInfo, PACL *pACL, Error **errp) 138 { 139 const int aclSize = 1; 140 PACL newACL = NULL; 141 EXPLICIT_ACCESS eAccess[1]; 142 PSID userPSID = NULL; 143 144 /* Get a pointer to the internal SID object in Windows */ 145 bool converted = ConvertStringSidToSid(userInfo->SSID, &userPSID); 146 if (!converted) { 147 error_setg_win32(errp, GetLastError(), "failed to retrieve user %s SID", 148 userInfo->username); 149 goto error; 150 } 151 152 /* Set the permissions for the user. */ 153 eAccess[0].grfAccessPermissions = GENERIC_ALL; 154 eAccess[0].grfAccessMode = SET_ACCESS; 155 eAccess[0].grfInheritance = NO_INHERITANCE; 156 eAccess[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; 157 eAccess[0].Trustee.TrusteeType = TRUSTEE_IS_USER; 158 eAccess[0].Trustee.ptstrName = (LPTSTR)userPSID; 159 160 /* Set the ACL entries */ 161 DWORD setResult; 162 163 /* 164 * If we are given a pointer that is already initialized, then we can merge 165 * the existing entries instead of overwriting them. 166 */ 167 if (*pACL) { 168 setResult = SetEntriesInAcl(aclSize, eAccess, *pACL, &newACL); 169 } else { 170 setResult = SetEntriesInAcl(aclSize, eAccess, NULL, &newACL); 171 } 172 173 if (setResult != ERROR_SUCCESS) { 174 error_setg_win32(errp, GetLastError(), 175 "failed to set ACL entries for user %s %lu", 176 userInfo->username, setResult); 177 goto error; 178 } 179 180 /* Free any old memory since we are going to overwrite the users pointer. */ 181 LocalFree(*pACL); 182 *pACL = newACL; 183 184 LocalFree(userPSID); 185 return true; 186 error: 187 LocalFree(userPSID); 188 return false; 189 } 190 191 /* 192 * Creates a base ACL for both normal users and admins to share 193 * pACL -> Pointer to an ACL structure 194 * errp -> Error structure to set any errors that occur 195 * returns: 1 on success, 0 otherwise 196 */ 197 static bool create_acl_base(PACL *pACL, Error **errp) 198 { 199 PSID adminGroupPSID = NULL; 200 PSID systemPSID = NULL; 201 202 const int aclSize = 2; 203 EXPLICIT_ACCESS eAccess[2]; 204 205 /* Create an entry for the system user. */ 206 const char *systemSID = LOCAL_SYSTEM_SID; 207 bool converted = ConvertStringSidToSid(systemSID, &systemPSID); 208 if (!converted) { 209 error_setg_win32(errp, GetLastError(), "failed to retrieve system SID"); 210 goto error; 211 } 212 213 /* set permissions for system user */ 214 eAccess[0].grfAccessPermissions = GENERIC_ALL; 215 eAccess[0].grfAccessMode = SET_ACCESS; 216 eAccess[0].grfInheritance = NO_INHERITANCE; 217 eAccess[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; 218 eAccess[0].Trustee.TrusteeType = TRUSTEE_IS_USER; 219 eAccess[0].Trustee.ptstrName = (LPTSTR)systemPSID; 220 221 /* Create an entry for the admin user. */ 222 const char *adminSID = ADMIN_SID; 223 converted = ConvertStringSidToSid(adminSID, &adminGroupPSID); 224 if (!converted) { 225 error_setg_win32(errp, GetLastError(), "failed to retrieve Admin SID"); 226 goto error; 227 } 228 229 /* Set permissions for admin group. */ 230 eAccess[1].grfAccessPermissions = GENERIC_ALL; 231 eAccess[1].grfAccessMode = SET_ACCESS; 232 eAccess[1].grfInheritance = NO_INHERITANCE; 233 eAccess[1].Trustee.TrusteeForm = TRUSTEE_IS_SID; 234 eAccess[1].Trustee.TrusteeType = TRUSTEE_IS_GROUP; 235 eAccess[1].Trustee.ptstrName = (LPTSTR)adminGroupPSID; 236 237 /* Put the entries in an ACL object. */ 238 PACL pNewACL = NULL; 239 DWORD setResult; 240 241 /* 242 *If we are given a pointer that is already initialized, then we can merge 243 *the existing entries instead of overwriting them. 244 */ 245 if (*pACL) { 246 setResult = SetEntriesInAcl(aclSize, eAccess, *pACL, &pNewACL); 247 } else { 248 setResult = SetEntriesInAcl(aclSize, eAccess, NULL, &pNewACL); 249 } 250 251 if (setResult != ERROR_SUCCESS) { 252 error_setg_win32(errp, GetLastError(), 253 "failed to set base ACL entries for system user and " 254 "admin group %lu", 255 setResult); 256 goto error; 257 } 258 259 LocalFree(adminGroupPSID); 260 LocalFree(systemPSID); 261 262 /* Free any old memory since we are going to overwrite the users pointer. */ 263 LocalFree(*pACL); 264 265 *pACL = pNewACL; 266 267 return true; 268 269 error: 270 LocalFree(adminGroupPSID); 271 LocalFree(systemPSID); 272 return false; 273 } 274 275 /* 276 * Sets the access control on the authorized_keys file and any ssh folders that 277 * need to be created. For administrators the required permissions on the 278 * file/folders are that only administrators and the LocalSystem account can 279 * access the folders. For normal user accounts only the specified user, 280 * LocalSystem and Administrators can have access to the key. 281 * 282 * parameters: 283 * userInfo -> pointer to structure that contains information about the user 284 * PACL -> pointer to an access control structure that will be set upon 285 * successful completion of the function. 286 * errp -> error structure that will be set upon error. 287 * returns: 1 upon success 0 upon failure. 288 */ 289 static bool create_acl(PWindowsUserInfo userInfo, PACL *pACL, Error **errp) 290 { 291 /* 292 * Creates a base ACL that both admins and users will share 293 * This adds the Administrators group and the SYSTEM group 294 */ 295 if (!create_acl_base(pACL, errp)) { 296 return false; 297 } 298 299 /* 300 * If the user is not an admin give the user creating the key permission to 301 * access the file. 302 */ 303 if (!userInfo->isAdmin) { 304 if (!create_acl_user(userInfo, pACL, errp)) { 305 return false; 306 } 307 308 return true; 309 } 310 311 return true; 312 } 313 /* 314 * Create the SSH directory for the user and d sets appropriate permissions. 315 * In general the directory will be %PROGRAMDATA%/ssh if the user is an admin. 316 * %USERPOFILE%/.ssh if not an admin 317 * 318 * parameters: 319 * userInfo -> Contains information about the user 320 * errp -> Structure that will contain errors if the function fails. 321 * returns: zero upon failure, 1 upon success 322 */ 323 static bool create_ssh_directory(WindowsUserInfo *userInfo, Error **errp) 324 { 325 PACL pNewACL = NULL; 326 g_autofree PSECURITY_DESCRIPTOR pSD = NULL; 327 328 /* Gets the appropriate ACL for the user */ 329 if (!create_acl(userInfo, &pNewACL, errp)) { 330 goto error; 331 } 332 333 /* Allocate memory for a security descriptor */ 334 pSD = g_malloc(SECURITY_DESCRIPTOR_MIN_LENGTH); 335 if (!InitializeSecurityDescriptor(pSD, SECURITY_DESCRIPTOR_REVISION)) { 336 error_setg_win32(errp, GetLastError(), 337 "Failed to initialize security descriptor"); 338 goto error; 339 } 340 341 /* Associate the security descriptor with the ACL permissions. */ 342 if (!SetSecurityDescriptorDacl(pSD, TRUE, pNewACL, FALSE)) { 343 error_setg_win32(errp, GetLastError(), 344 "Failed to set security descriptor ACL"); 345 goto error; 346 } 347 348 /* Set the security attributes on the folder */ 349 SECURITY_ATTRIBUTES sAttr; 350 sAttr.bInheritHandle = FALSE; 351 sAttr.nLength = sizeof(SECURITY_ATTRIBUTES); 352 sAttr.lpSecurityDescriptor = pSD; 353 354 /* Create the directory with the created permissions */ 355 BOOL created = CreateDirectory(userInfo->sshDirectory, &sAttr); 356 if (!created) { 357 error_setg_win32(errp, GetLastError(), "failed to create directory %s", 358 userInfo->sshDirectory); 359 goto error; 360 } 361 362 /* Free memory */ 363 LocalFree(pNewACL); 364 return true; 365 error: 366 LocalFree(pNewACL); 367 return false; 368 } 369 370 /* 371 * Sets permissions on the authorized_key_file that is created. 372 * 373 * parameters: userInfo -> Information about the user 374 * errp -> error structure that will contain errors upon failure 375 * returns: 1 upon success, zero upon failure. 376 */ 377 static bool set_file_permissions(PWindowsUserInfo userInfo, Error **errp) 378 { 379 PACL pACL = NULL; 380 PSID userPSID = NULL; 381 382 /* Creates the access control structure */ 383 if (!create_acl(userInfo, &pACL, errp)) { 384 goto error; 385 } 386 387 /* Get the PSID structure for the user based off the string SID. */ 388 bool converted = ConvertStringSidToSid(userInfo->SSID, &userPSID); 389 if (!converted) { 390 error_setg_win32(errp, GetLastError(), "failed to retrieve user %s SID", 391 userInfo->username); 392 goto error; 393 } 394 395 /* Prevents permissions from being inherited and use the DACL provided. */ 396 const SE_OBJECT_TYPE securityBitFlags = 397 DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION; 398 399 /* Set the ACL on the file. */ 400 if (SetNamedSecurityInfo(userInfo->authorizedKeyFile, SE_FILE_OBJECT, 401 securityBitFlags, userPSID, NULL, pACL, 402 NULL) != ERROR_SUCCESS) { 403 error_setg_win32(errp, GetLastError(), 404 "failed to set file security for file %s", 405 userInfo->authorizedKeyFile); 406 goto error; 407 } 408 409 LocalFree(pACL); 410 LocalFree(userPSID); 411 return true; 412 413 error: 414 LocalFree(pACL); 415 LocalFree(userPSID); 416 417 return false; 418 } 419 420 /* 421 * Writes the specified keys to the authenticated keys file. 422 * parameters: 423 * userInfo: Information about the user we are writing the authkeys file to. 424 * authkeys: Array of keys to write to disk 425 * errp: Error structure that will contain any errors if they occur. 426 * returns: 1 if successful, 0 otherwise. 427 */ 428 static bool write_authkeys(WindowsUserInfo *userInfo, GStrv authkeys, 429 Error **errp) 430 { 431 g_autofree char *contents = NULL; 432 g_autoptr(GError) err = NULL; 433 434 contents = g_strjoinv("\n", authkeys); 435 436 if (!g_file_set_contents(userInfo->authorizedKeyFile, contents, -1, &err)) { 437 error_setg(errp, "failed to write to '%s': %s", 438 userInfo->authorizedKeyFile, err->message); 439 return false; 440 } 441 442 if (!set_file_permissions(userInfo, errp)) { 443 return false; 444 } 445 446 return true; 447 } 448 449 /* 450 * Retrieves information about a Windows user by their username 451 * 452 * parameters: 453 * userInfo -> Double pointer to a WindowsUserInfo structure. Upon success, it 454 * will be allocated with information about the user and need to be freed. 455 * username -> Name of the user to lookup. 456 * errp -> Contains any errors that occur. 457 * returns: 1 upon success, 0 upon failure. 458 */ 459 static bool get_user_info(PWindowsUserInfo *userInfo, const char *username, 460 Error **errp) 461 { 462 DWORD infoLevel = 4; 463 LPUSER_INFO_4 uBuf = NULL; 464 g_autofree wchar_t *wideUserName = NULL; 465 g_autoptr(GError) gerr = NULL; 466 PSID psid = NULL; 467 468 /* 469 * Converts a string to a Windows wide string since the GetNetUserInfo 470 * function requires it. 471 */ 472 wideUserName = g_utf8_to_utf16(username, -1, NULL, NULL, &gerr); 473 if (!wideUserName) { 474 goto error; 475 } 476 477 /* allocate data */ 478 PWindowsUserInfo uData = g_new0(WindowsUserInfo, 1); 479 480 /* Set pointer so it can be cleaned up by the callee, even upon error. */ 481 *userInfo = uData; 482 483 /* Find the information */ 484 NET_API_STATUS result = 485 NetUserGetInfo(NULL, wideUserName, infoLevel, (LPBYTE *)&uBuf); 486 if (result != NERR_Success) { 487 /* Give a friendlier error message if the user was not found. */ 488 if (result == NERR_UserNotFound) { 489 error_setg(errp, "User %s was not found", username); 490 goto error; 491 } 492 493 error_setg(errp, 494 "Received unexpected error when asking for user info: Error " 495 "Code %lu", 496 result); 497 goto error; 498 } 499 500 /* Get information from the buffer returned by NetUserGetInfo. */ 501 uData->username = g_strdup(username); 502 uData->isAdmin = uBuf->usri4_priv == USER_PRIV_ADMIN; 503 psid = uBuf->usri4_user_sid; 504 505 char *sidStr = NULL; 506 507 /* 508 * We store the string representation of the SID not SID structure in 509 * memory. Callees wanting to use the SID structure should call 510 * ConvertStringSidToSID. 511 */ 512 if (!ConvertSidToStringSid(psid, &sidStr)) { 513 error_setg_win32(errp, GetLastError(), 514 "failed to get SID string for user %s", username); 515 goto error; 516 } 517 518 /* Store the SSID */ 519 uData->SSID = sidStr; 520 521 /* Get the SSH folder for the user. */ 522 char *sshFolder = get_ssh_folder(username, uData->isAdmin, errp); 523 if (sshFolder == NULL) { 524 goto error; 525 } 526 527 /* Get the authorized key file path */ 528 const char *authorizedKeyFile = 529 uData->isAdmin ? AUTHORIZED_KEY_FILE_ADMIN : AUTHORIZED_KEY_FILE; 530 char *authorizedKeyPath = 531 g_build_filename(sshFolder, authorizedKeyFile, NULL); 532 uData->sshDirectory = sshFolder; 533 uData->authorizedKeyFile = authorizedKeyPath; 534 535 /* Free */ 536 NetApiBufferFree(uBuf); 537 return true; 538 error: 539 if (uBuf) { 540 NetApiBufferFree(uBuf); 541 } 542 543 return false; 544 } 545 546 /* 547 * Gets the list of authorized keys for a user. 548 * 549 * parameters: 550 * username -> Username to retrieve the keys for. 551 * errp -> Error structure that will display any errors through QMP. 552 * returns: List of keys associated with the user. 553 */ 554 GuestAuthorizedKeys *qmp_guest_ssh_get_authorized_keys(const char *username, 555 Error **errp) 556 { 557 GuestAuthorizedKeys *keys = NULL; 558 g_auto(GStrv) authKeys = NULL; 559 g_autoptr(GuestAuthorizedKeys) ret = NULL; 560 g_auto(PWindowsUserInfo) userInfo = NULL; 561 562 /* Gets user information */ 563 if (!get_user_info(&userInfo, username, errp)) { 564 return NULL; 565 } 566 567 /* Reads authkeys for the user */ 568 authKeys = read_authkeys(userInfo->authorizedKeyFile, errp); 569 if (authKeys == NULL) { 570 return NULL; 571 } 572 573 /* Set the GuestAuthorizedKey struct with keys from the file */ 574 ret = g_new0(GuestAuthorizedKeys, 1); 575 for (int i = 0; authKeys[i] != NULL; i++) { 576 g_strstrip(authKeys[i]); 577 if (!authKeys[i][0] || authKeys[i][0] == '#') { 578 continue; 579 } 580 581 QAPI_LIST_PREPEND(ret->keys, g_strdup(authKeys[i])); 582 } 583 584 /* 585 * Steal the pointer because it is up for the callee to deallocate the 586 * memory. 587 */ 588 keys = g_steal_pointer(&ret); 589 return keys; 590 } 591 592 /* 593 * Adds an ssh key for a user. 594 * 595 * parameters: 596 * username -> User to add the SSH key to 597 * strList -> Array of keys to add to the list 598 * has_reset -> Whether the keys have been reset 599 * reset -> Boolean to reset the keys (If this is set the existing list will be 600 * cleared) and the other key reset. errp -> Pointer to an error structure that 601 * will get returned over QMP if anything goes wrong. 602 */ 603 void qmp_guest_ssh_add_authorized_keys(const char *username, strList *keys, 604 bool has_reset, bool reset, Error **errp) 605 { 606 g_auto(PWindowsUserInfo) userInfo = NULL; 607 g_auto(GStrv) authkeys = NULL; 608 strList *k; 609 size_t nkeys, nauthkeys; 610 611 /* Make sure the keys given are valid */ 612 if (!check_openssh_pub_keys(keys, &nkeys, errp)) { 613 return; 614 } 615 616 /* Gets user information */ 617 if (!get_user_info(&userInfo, username, errp)) { 618 return; 619 } 620 621 /* Determine whether we should reset the keys */ 622 reset = has_reset && reset; 623 if (!reset) { 624 /* Read existing keys into memory */ 625 authkeys = read_authkeys(userInfo->authorizedKeyFile, NULL); 626 } 627 628 /* Check that the SSH key directory exists for the user. */ 629 if (!g_file_test(userInfo->sshDirectory, G_FILE_TEST_IS_DIR)) { 630 BOOL success = create_ssh_directory(userInfo, errp); 631 if (!success) { 632 return; 633 } 634 } 635 636 /* Reallocates the buffer to fit the new keys. */ 637 nauthkeys = authkeys ? g_strv_length(authkeys) : 0; 638 authkeys = g_realloc_n(authkeys, nauthkeys + nkeys + 1, sizeof(char *)); 639 640 /* zero out the memory for the reallocated buffer */ 641 memset(authkeys + nauthkeys, 0, (nkeys + 1) * sizeof(char *)); 642 643 /* Adds the keys */ 644 for (k = keys; k != NULL; k = k->next) { 645 /* Check that the key doesn't already exist */ 646 if (g_strv_contains((const gchar *const *)authkeys, k->value)) { 647 continue; 648 } 649 650 authkeys[nauthkeys++] = g_strdup(k->value); 651 } 652 653 /* Write the authkeys to the file. */ 654 write_authkeys(userInfo, authkeys, errp); 655 } 656 657 /* 658 * Removes an SSH key for a user 659 * 660 * parameters: 661 * username -> Username to remove the key from 662 * strList -> List of strings to remove 663 * errp -> Contains any errors that occur. 664 */ 665 void qmp_guest_ssh_remove_authorized_keys(const char *username, strList *keys, 666 Error **errp) 667 { 668 g_auto(PWindowsUserInfo) userInfo = NULL; 669 g_autofree struct passwd *p = NULL; 670 g_autofree GStrv new_keys = NULL; /* do not own the strings */ 671 g_auto(GStrv) authkeys = NULL; 672 GStrv a; 673 size_t nkeys = 0; 674 675 /* Validates the keys passed in by the user */ 676 if (!check_openssh_pub_keys(keys, NULL, errp)) { 677 return; 678 } 679 680 /* Gets user information */ 681 if (!get_user_info(&userInfo, username, errp)) { 682 return; 683 } 684 685 /* Reads the authkeys for the user */ 686 authkeys = read_authkeys(userInfo->authorizedKeyFile, errp); 687 if (authkeys == NULL) { 688 return; 689 } 690 691 /* Create a new buffer to hold the keys */ 692 new_keys = g_new0(char *, g_strv_length(authkeys) + 1); 693 for (a = authkeys; *a != NULL; a++) { 694 strList *k; 695 696 /* Filters out keys that are equal to ones the user specified. */ 697 for (k = keys; k != NULL; k = k->next) { 698 if (g_str_equal(k->value, *a)) { 699 break; 700 } 701 } 702 703 if (k != NULL) { 704 continue; 705 } 706 707 new_keys[nkeys++] = *a; 708 } 709 710 /* Write the new authkeys to the file. */ 711 write_authkeys(userInfo, new_keys, errp); 712 } 713