2 * QEMU Guest Agent win32-specific command implementations for SSH keys.
3 * The implementation is opinionated and expects the SSH implementation to
6 * Copyright Schweitzer Engineering Laboratories. 2024
9 * Aidan Leuck <aidan_leuck@selinc.com>
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.
15 #include "qemu/osdep.h"
17 #include <qga-qapi-types.h>
19 #include "commands-common-ssh.h"
20 #include "commands-windows-ssh.h"
21 #include "guest-agent-core.h"
26 #include "qapi/error.h"
28 #include "qga-qapi-commands.h"
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"
39 * Frees userInfo structure. This implements the g_auto cleanup
42 void free_userInfo(PWindowsUserInfo info
)
44 g_free(info
->sshDirectory
);
45 g_free(info
->authorizedKeyFile
);
46 LocalFree(info
->SSID
);
47 g_free(info
->username
);
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
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
62 static char *get_admin_ssh_folder(Error
**errp
)
64 /* Allocate memory for the program data path */
65 g_autofree
char *programDataPath
= NULL
;
66 char *authkeys_path
= NULL
;
68 g_autoptr(GError
) gerr
= NULL
;
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");
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
) {
83 "Failed converting ProgramData folder path to UTF-16 %s",
88 /* Build the path to the file. */
89 authkeys_path
= g_build_filename(programDataPath
, "ssh", NULL
);
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
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.
104 static char *get_ssh_folder(const char *username
, const bool isAdmin
,
107 DWORD maxSize
= MAX_PATH
;
108 g_autofree
char *profilesDir
= g_new0(char, maxSize
);
111 return get_admin_ssh_folder(errp
);
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
);
118 error_setg_win32(errp
, GetLastError(),
119 "failed to retrieve profiles directory");
123 /* Builds the filename */
124 return g_build_filename(profilesDir
, username
, ".ssh", NULL
);
128 * Creates an entry for the user so they can access the ssh folder in their
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
137 static bool create_acl_user(PWindowsUserInfo userInfo
, PACL
*pACL
, Error
**errp
)
139 const int aclSize
= 1;
141 EXPLICIT_ACCESS eAccess
[1];
142 PSID userPSID
= NULL
;
144 /* Get a pointer to the internal SID object in Windows */
145 bool converted
= ConvertStringSidToSid(userInfo
->SSID
, &userPSID
);
147 error_setg_win32(errp
, GetLastError(), "failed to retrieve user %s SID",
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
;
160 /* Set the ACL entries */
164 * If we are given a pointer that is already initialized, then we can merge
165 * the existing entries instead of overwriting them.
168 setResult
= SetEntriesInAcl(aclSize
, eAccess
, *pACL
, &newACL
);
170 setResult
= SetEntriesInAcl(aclSize
, eAccess
, NULL
, &newACL
);
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
);
180 /* Free any old memory since we are going to overwrite the users pointer. */
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
197 static bool create_acl_base(PACL
*pACL
, Error
**errp
)
199 PSID adminGroupPSID
= NULL
;
200 PSID systemPSID
= NULL
;
202 const int aclSize
= 2;
203 EXPLICIT_ACCESS eAccess
[2];
205 /* Create an entry for the system user. */
206 const char *systemSID
= LOCAL_SYSTEM_SID
;
207 bool converted
= ConvertStringSidToSid(systemSID
, &systemPSID
);
209 error_setg_win32(errp
, GetLastError(), "failed to retrieve system SID");
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
;
221 /* Create an entry for the admin user. */
222 const char *adminSID
= ADMIN_SID
;
223 converted
= ConvertStringSidToSid(adminSID
, &adminGroupPSID
);
225 error_setg_win32(errp
, GetLastError(), "failed to retrieve Admin SID");
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
;
237 /* Put the entries in an ACL object. */
242 *If we are given a pointer that is already initialized, then we can merge
243 *the existing entries instead of overwriting them.
246 setResult
= SetEntriesInAcl(aclSize
, eAccess
, *pACL
, &pNewACL
);
248 setResult
= SetEntriesInAcl(aclSize
, eAccess
, NULL
, &pNewACL
);
251 if (setResult
!= ERROR_SUCCESS
) {
252 error_setg_win32(errp
, GetLastError(),
253 "failed to set base ACL entries for system user and "
259 LocalFree(adminGroupPSID
);
260 LocalFree(systemPSID
);
262 /* Free any old memory since we are going to overwrite the users pointer. */
270 LocalFree(adminGroupPSID
);
271 LocalFree(systemPSID
);
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.
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.
289 static bool create_acl(PWindowsUserInfo userInfo
, PACL
*pACL
, Error
**errp
)
292 * Creates a base ACL that both admins and users will share
293 * This adds the Administrators group and the SYSTEM group
295 if (!create_acl_base(pACL
, errp
)) {
300 * If the user is not an admin give the user creating the key permission to
303 if (!userInfo
->isAdmin
) {
304 if (!create_acl_user(userInfo
, pACL
, errp
)) {
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
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
323 static bool create_ssh_directory(WindowsUserInfo
*userInfo
, Error
**errp
)
326 g_autofree PSECURITY_DESCRIPTOR pSD
= NULL
;
328 /* Gets the appropriate ACL for the user */
329 if (!create_acl(userInfo
, &pNewACL
, errp
)) {
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");
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");
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
;
354 /* Create the directory with the created permissions */
355 BOOL created
= CreateDirectory(userInfo
->sshDirectory
, &sAttr
);
357 error_setg_win32(errp
, GetLastError(), "failed to create directory %s",
358 userInfo
->sshDirectory
);
371 * Sets permissions on the authorized_key_file that is created.
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.
377 static bool set_file_permissions(PWindowsUserInfo userInfo
, Error
**errp
)
382 /* Creates the access control structure */
383 if (!create_acl(userInfo
, &pACL
, errp
)) {
387 /* Get the PSID structure for the user based off the string SID. */
388 bool converted
= ConvertStringSidToSid(userInfo
->SSID
, &userPSID
);
390 error_setg_win32(errp
, GetLastError(), "failed to retrieve user %s SID",
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
;
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
);
421 * Writes the specified keys to the authenticated keys file.
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.
428 static bool write_authkeys(WindowsUserInfo
*userInfo
, GStrv authkeys
,
431 g_autofree
char *contents
= NULL
;
432 g_autoptr(GError
) err
= NULL
;
434 contents
= g_strjoinv("\n", authkeys
);
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
);
442 if (!set_file_permissions(userInfo
, errp
)) {
450 * Retrieves information about a Windows user by their username
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.
459 static bool get_user_info(PWindowsUserInfo
*userInfo
, const char *username
,
463 LPUSER_INFO_4 uBuf
= NULL
;
464 g_autofree
wchar_t *wideUserName
= NULL
;
465 g_autoptr(GError
) gerr
= NULL
;
469 * Converts a string to a Windows wide string since the GetNetUserInfo
470 * function requires it.
472 wideUserName
= g_utf8_to_utf16(username
, -1, NULL
, NULL
, &gerr
);
478 PWindowsUserInfo uData
= g_new0(WindowsUserInfo
, 1);
480 /* Set pointer so it can be cleaned up by the callee, even upon error. */
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
);
494 "Received unexpected error when asking for user info: Error "
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
;
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.
512 if (!ConvertSidToStringSid(psid
, &sidStr
)) {
513 error_setg_win32(errp
, GetLastError(),
514 "failed to get SID string for user %s", username
);
519 uData
->SSID
= sidStr
;
521 /* Get the SSH folder for the user. */
522 char *sshFolder
= get_ssh_folder(username
, uData
->isAdmin
, errp
);
523 if (sshFolder
== NULL
) {
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
;
536 NetApiBufferFree(uBuf
);
540 NetApiBufferFree(uBuf
);
547 * Gets the list of authorized keys for a user.
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.
554 GuestAuthorizedKeys
*qmp_guest_ssh_get_authorized_keys(const char *username
,
557 GuestAuthorizedKeys
*keys
= NULL
;
558 g_auto(GStrv
) authKeys
= NULL
;
559 g_autoptr(GuestAuthorizedKeys
) ret
= NULL
;
560 g_auto(PWindowsUserInfo
) userInfo
= NULL
;
562 /* Gets user information */
563 if (!get_user_info(&userInfo
, username
, errp
)) {
567 /* Reads authkeys for the user */
568 authKeys
= read_authkeys(userInfo
->authorizedKeyFile
, errp
);
569 if (authKeys
== NULL
) {
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] == '#') {
581 QAPI_LIST_PREPEND(ret
->keys
, g_strdup(authKeys
[i
]));
585 * Steal the pointer because it is up for the callee to deallocate the
588 keys
= g_steal_pointer(&ret
);
593 * Adds an ssh key for a user.
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.
603 void qmp_guest_ssh_add_authorized_keys(const char *username
, strList
*keys
,
604 bool has_reset
, bool reset
, Error
**errp
)
606 g_auto(PWindowsUserInfo
) userInfo
= NULL
;
607 g_auto(GStrv
) authkeys
= NULL
;
609 size_t nkeys
, nauthkeys
;
611 /* Make sure the keys given are valid */
612 if (!check_openssh_pub_keys(keys
, &nkeys
, errp
)) {
616 /* Gets user information */
617 if (!get_user_info(&userInfo
, username
, errp
)) {
621 /* Determine whether we should reset the keys */
622 reset
= has_reset
&& reset
;
624 /* Read existing keys into memory */
625 authkeys
= read_authkeys(userInfo
->authorizedKeyFile
, NULL
);
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
);
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 *));
640 /* zero out the memory for the reallocated buffer */
641 memset(authkeys
+ nauthkeys
, 0, (nkeys
+ 1) * sizeof(char *));
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
)) {
650 authkeys
[nauthkeys
++] = g_strdup(k
->value
);
653 /* Write the authkeys to the file. */
654 write_authkeys(userInfo
, authkeys
, errp
);
658 * Removes an SSH key for a user
661 * username -> Username to remove the key from
662 * strList -> List of strings to remove
663 * errp -> Contains any errors that occur.
665 void qmp_guest_ssh_remove_authorized_keys(const char *username
, strList
*keys
,
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
;
675 /* Validates the keys passed in by the user */
676 if (!check_openssh_pub_keys(keys
, NULL
, errp
)) {
680 /* Gets user information */
681 if (!get_user_info(&userInfo
, username
, errp
)) {
685 /* Reads the authkeys for the user */
686 authkeys
= read_authkeys(userInfo
->authorizedKeyFile
, errp
);
687 if (authkeys
== NULL
) {
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
++) {
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
)) {
707 new_keys
[nkeys
++] = *a
;
710 /* Write the new authkeys to the file. */
711 write_authkeys(userInfo
, new_keys
, errp
);