xref: /openbmc/qemu/qga/commands-windows-ssh.c (revision b86c6ba6)
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;
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