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