* [PATCH v8 0/2] Implement SSH commands in QEMU GA for Windows
@ 2024-04-23 15:00 aidan_leuck
2024-04-23 15:00 ` [PATCH v8 1/2] Refactor common functions between POSIX and Windows implementation aidan_leuck
2024-04-23 15:00 ` [PATCH v8 2/2] Implement SSH commands in QEMU GA for Windows aidan_leuck
0 siblings, 2 replies; 8+ messages in thread
From: aidan_leuck @ 2024-04-23 15:00 UTC (permalink / raw)
To: qemu-devel; +Cc: kkostiuk, aidaleuc
From: aidaleuc <aidan_leuck@selinc.com>
This patch aims to implement guest-ssh-add-authorized-keys, guest-ssh-remove-authorized-keys, and guest-ssh-get-authorized-keys
for Windows. This PR is based on Microsoft's OpenSSH implementation https://github.com/PowerShell/Win32-OpenSSH. The guest agents
will support Kubevirt and allow guest agent propagation to be used to dynamically inject SSH keys.
https://kubevirt.io/user-guide/virtual_machines/accessing_virtual_machines/#dynamic-ssh-public-key-injection-via-qemu-guest-agent
Changes since v7
* Fixed missing line ending. Thanks IDE.
Changes since v6
* Fixed issue with file permissions causing public keys to be rejected by the SSH server. credit (kkostiuk)
Older versions of OpenSSH such as the one shipped with Windows Server 2022 are more stringent on
file permissions.
* Removed everyone group from create_acl
* Disabled public key file inheritance from the parent folder to control permissions in a more granular fashion
Changes since v5
* Fixed spurious formatting
Changes since v4
* Moved qapi/error.h to commands-common-ssh.c
* Changed <qga-qapi-types.h> to "qapi/qapi-builtin-types.h"
* Removed stbool.h from commands-common-ssh.h
Changes since v3
* Renamed commands-ssh-core.c/h to commands-common-ssh.c/h
* Fixed styling errors discovered by checkpatch.pl
* Moved some header includes to the commands-common-ssh.h
Changes since v2
* Set indent to 4 spaces
* Moved all comments to C style comments
* Fixed a segfault bug in get_user_info function related to non zeroed memory when a user did not exist.
* Used g_new0 instead of g_malloc where applicable
* Modified newlines in qapi-schema.json
* Added newlines at the end of all files
* GError functions now use g_autoptr instead of being freed manually.
* Refactored get_ssh_folder to remove goto error statement
* Fixed uninitialized variable pgDataW
* Modified patch order so that the generalization patch is the first patch
* Removed unnecssary ZeroMemory calls
Changes since v1
* Fixed styling errors
* Moved from wcstombs to g_utf functions
* Removed unnecessary if checks on calls to free
* Fixed copyright headers
* Refactored create_acl functions into base function, admin function and user function
* Removed unused user count function
* Split up refactor of existing code into a separate patch
aidaleuc (2):
Refactor common functions between POSIX and Windows implementation
Implement SSH commands in QEMU GA for Windows
qga/commands-common-ssh.c | 50 +++
qga/commands-common-ssh.h | 10 +
qga/commands-posix-ssh.c | 47 +--
qga/commands-windows-ssh.c | 712 +++++++++++++++++++++++++++++++++++++
qga/commands-windows-ssh.h | 26 ++
qga/meson.build | 6 +-
qga/qapi-schema.json | 17 +-
7 files changed, 811 insertions(+), 57 deletions(-)
create mode 100644 qga/commands-common-ssh.c
create mode 100644 qga/commands-common-ssh.h
create mode 100644 qga/commands-windows-ssh.c
create mode 100644 qga/commands-windows-ssh.h
--
2.34.1
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v8 1/2] Refactor common functions between POSIX and Windows implementation
2024-04-23 15:00 [PATCH v8 0/2] Implement SSH commands in QEMU GA for Windows aidan_leuck
@ 2024-04-23 15:00 ` aidan_leuck
2024-04-24 9:28 ` Konstantin Kostiuk
2024-04-23 15:00 ` [PATCH v8 2/2] Implement SSH commands in QEMU GA for Windows aidan_leuck
1 sibling, 1 reply; 8+ messages in thread
From: aidan_leuck @ 2024-04-23 15:00 UTC (permalink / raw)
To: qemu-devel; +Cc: kkostiuk, aidaleuc
From: aidaleuc <aidan_leuck@selinc.com>
Signed-off-by: aidaleuc <aidan_leuck@selinc.com>
---
qga/commands-common-ssh.c | 50 +++++++++++++++++++++++++++++++++++++++
qga/commands-common-ssh.h | 10 ++++++++
qga/commands-posix-ssh.c | 47 +-----------------------------------
qga/meson.build | 1 +
4 files changed, 62 insertions(+), 46 deletions(-)
create mode 100644 qga/commands-common-ssh.c
create mode 100644 qga/commands-common-ssh.h
diff --git a/qga/commands-common-ssh.c b/qga/commands-common-ssh.c
new file mode 100644
index 0000000000..537869fb98
--- /dev/null
+++ b/qga/commands-common-ssh.c
@@ -0,0 +1,50 @@
+/*
+ * 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 "qapi/error.h"
+#include "commands-common-ssh.h"
+
+GStrv read_authkeys(const char *path, Error **errp)
+{
+ g_autoptr(GError) err = NULL;
+ g_autofree char *contents = NULL;
+
+ if (!g_file_get_contents(path, &contents, NULL, &err)) {
+ error_setg(errp, "failed to read '%s': %s", path, err->message);
+ return NULL;
+ }
+
+ return g_strsplit(contents, "\n", -1);
+}
+
+bool check_openssh_pub_keys(strList *keys, size_t *nkeys, Error **errp)
+{
+ size_t n = 0;
+ strList *k;
+
+ for (k = keys; k != NULL; k = k->next) {
+ if (!check_openssh_pub_key(k->value, errp)) {
+ return false;
+ }
+ n++;
+ }
+
+ if (nkeys) {
+ *nkeys = n;
+ }
+ return true;
+}
+
+bool check_openssh_pub_key(const char *key, Error **errp)
+{
+ /* simple sanity-check, we may want more? */
+ if (!key || key[0] == '#' || strchr(key, '\n')) {
+ error_setg(errp, "invalid OpenSSH public key: '%s'", key);
+ return false;
+ }
+
+ return true;
+}
diff --git a/qga/commands-common-ssh.h b/qga/commands-common-ssh.h
new file mode 100644
index 0000000000..14d955fa84
--- /dev/null
+++ b/qga/commands-common-ssh.h
@@ -0,0 +1,10 @@
+/*
+ * 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 "qapi/qapi-builtin-types.h"
+
+GStrv read_authkeys(const char *path, Error **errp);
+bool check_openssh_pub_keys(strList *keys, size_t *nkeys, Error **errp);
+bool check_openssh_pub_key(const char *key, Error **errp);
diff --git a/qga/commands-posix-ssh.c b/qga/commands-posix-ssh.c
index 236f80de44..dd2ecb453a 100644
--- a/qga/commands-posix-ssh.c
+++ b/qga/commands-posix-ssh.c
@@ -9,6 +9,7 @@
#include <locale.h>
#include <pwd.h>
+#include "commands-common-ssh.h"
#include "qapi/error.h"
#include "qga-qapi-commands.h"
@@ -80,37 +81,6 @@ mkdir_for_user(const char *path, const struct passwd *p,
return true;
}
-static bool
-check_openssh_pub_key(const char *key, Error **errp)
-{
- /* simple sanity-check, we may want more? */
- if (!key || key[0] == '#' || strchr(key, '\n')) {
- error_setg(errp, "invalid OpenSSH public key: '%s'", key);
- return false;
- }
-
- return true;
-}
-
-static bool
-check_openssh_pub_keys(strList *keys, size_t *nkeys, Error **errp)
-{
- size_t n = 0;
- strList *k;
-
- for (k = keys; k != NULL; k = k->next) {
- if (!check_openssh_pub_key(k->value, errp)) {
- return false;
- }
- n++;
- }
-
- if (nkeys) {
- *nkeys = n;
- }
- return true;
-}
-
static bool
write_authkeys(const char *path, const GStrv keys,
const struct passwd *p, Error **errp)
@@ -139,21 +109,6 @@ write_authkeys(const char *path, const GStrv keys,
return true;
}
-static GStrv
-read_authkeys(const char *path, Error **errp)
-{
- g_autoptr(GError) err = NULL;
- g_autofree char *contents = NULL;
-
- if (!g_file_get_contents(path, &contents, NULL, &err)) {
- error_setg(errp, "failed to read '%s': %s", path, err->message);
- return NULL;
- }
-
- return g_strsplit(contents, "\n", -1);
-
-}
-
void
qmp_guest_ssh_add_authorized_keys(const char *username, strList *keys,
bool has_reset, bool reset,
diff --git a/qga/meson.build b/qga/meson.build
index 1c3d2a3d1b..4c3899751b 100644
--- a/qga/meson.build
+++ b/qga/meson.build
@@ -66,6 +66,7 @@ qga_ss.add(files(
'guest-agent-command-state.c',
'main.c',
'cutils.c',
+ 'commands-common-ssh.c'
))
if host_os == 'windows'
qga_ss.add(files(
--
2.34.1
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v8 2/2] Implement SSH commands in QEMU GA for Windows
2024-04-23 15:00 [PATCH v8 0/2] Implement SSH commands in QEMU GA for Windows aidan_leuck
2024-04-23 15:00 ` [PATCH v8 1/2] Refactor common functions between POSIX and Windows implementation aidan_leuck
@ 2024-04-23 15:00 ` aidan_leuck
2024-04-24 8:55 ` Dehan Meng
` (2 more replies)
1 sibling, 3 replies; 8+ messages in thread
From: aidan_leuck @ 2024-04-23 15:00 UTC (permalink / raw)
To: qemu-devel; +Cc: kkostiuk, aidaleuc
From: aidaleuc <aidan_leuck@selinc.com>
Signed-off-by: aidaleuc <aidan_leuck@selinc.com>
---
qga/commands-windows-ssh.c | 712 +++++++++++++++++++++++++++++++++++++
qga/commands-windows-ssh.h | 26 ++
qga/meson.build | 5 +-
qga/qapi-schema.json | 17 +-
4 files changed, 749 insertions(+), 11 deletions(-)
create mode 100644 qga/commands-windows-ssh.c
create mode 100644 qga/commands-windows-ssh.h
diff --git a/qga/commands-windows-ssh.c b/qga/commands-windows-ssh.c
new file mode 100644
index 0000000000..6a642e3ba8
--- /dev/null
+++ b/qga/commands-windows-ssh.c
@@ -0,0 +1,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);
+}
diff --git a/qga/commands-windows-ssh.h b/qga/commands-windows-ssh.h
new file mode 100644
index 0000000000..40ac67c4d9
--- /dev/null
+++ b/qga/commands-windows-ssh.h
@@ -0,0 +1,26 @@
+/*
+ * Header file for commands-windows-ssh.c
+ *
+ * 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 <glib/gstrfuncs.h>
+#include <stdbool.h>
+typedef struct WindowsUserInfo {
+ char *sshDirectory;
+ char *authorizedKeyFile;
+ char *username;
+ char *SSID;
+ bool isAdmin;
+} WindowsUserInfo;
+
+typedef WindowsUserInfo *PWindowsUserInfo;
+
+void free_userInfo(PWindowsUserInfo info);
+G_DEFINE_AUTO_CLEANUP_FREE_FUNC(PWindowsUserInfo, free_userInfo, NULL);
diff --git a/qga/meson.build b/qga/meson.build
index 4c3899751b..d5de492da3 100644
--- a/qga/meson.build
+++ b/qga/meson.build
@@ -73,7 +73,8 @@ if host_os == 'windows'
'channel-win32.c',
'commands-win32.c',
'service-win32.c',
- 'vss-win32.c'
+ 'vss-win32.c',
+ 'commands-windows-ssh.c'
))
else
qga_ss.add(files(
@@ -94,7 +95,7 @@ gen_tlb = []
qga_libs = []
if host_os == 'windows'
qga_libs += ['-lws2_32', '-lwinmm', '-lpowrprof', '-lwtsapi32', '-lwininet', '-liphlpapi', '-lnetapi32',
- '-lsetupapi', '-lcfgmgr32']
+ '-lsetupapi', '-lcfgmgr32', '-luserenv']
if have_qga_vss
qga_libs += ['-lole32', '-loleaut32', '-lshlwapi', '-lstdc++', '-Wl,--enable-stdcall-fixup']
subdir('vss-win32')
diff --git a/qga/qapi-schema.json b/qga/qapi-schema.json
index 9554b566a7..a64a6d91cf 100644
--- a/qga/qapi-schema.json
+++ b/qga/qapi-schema.json
@@ -1562,9 +1562,8 @@
{ 'struct': 'GuestAuthorizedKeys',
'data': {
'keys': ['str']
- },
- 'if': 'CONFIG_POSIX' }
-
+ }
+}
##
# @guest-ssh-get-authorized-keys:
@@ -1580,8 +1579,8 @@
##
{ 'command': 'guest-ssh-get-authorized-keys',
'data': { 'username': 'str' },
- 'returns': 'GuestAuthorizedKeys',
- 'if': 'CONFIG_POSIX' }
+ 'returns': 'GuestAuthorizedKeys'
+}
##
# @guest-ssh-add-authorized-keys:
@@ -1599,8 +1598,8 @@
# Since: 5.2
##
{ 'command': 'guest-ssh-add-authorized-keys',
- 'data': { 'username': 'str', 'keys': ['str'], '*reset': 'bool' },
- 'if': 'CONFIG_POSIX' }
+ 'data': { 'username': 'str', 'keys': ['str'], '*reset': 'bool' }
+}
##
# @guest-ssh-remove-authorized-keys:
@@ -1617,8 +1616,8 @@
# Since: 5.2
##
{ 'command': 'guest-ssh-remove-authorized-keys',
- 'data': { 'username': 'str', 'keys': ['str'] },
- 'if': 'CONFIG_POSIX' }
+ 'data': { 'username': 'str', 'keys': ['str'] }
+}
##
# @GuestDiskStats:
--
2.34.1
^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH v8 2/2] Implement SSH commands in QEMU GA for Windows
2024-04-23 15:00 ` [PATCH v8 2/2] Implement SSH commands in QEMU GA for Windows aidan_leuck
@ 2024-04-24 8:55 ` Dehan Meng
2024-04-24 9:21 ` Dehan Meng
2024-04-24 9:27 ` Konstantin Kostiuk
2 siblings, 0 replies; 8+ messages in thread
From: Dehan Meng @ 2024-04-24 8:55 UTC (permalink / raw)
To: aidan_leuck; +Cc: qemu-devel, kkostiuk
[-- Attachment #1: Type: text/plain, Size: 28902 bytes --]
QE tested this series's patches. SSH-relevant commands work and
password-free login can be achieved.
Tested-by: Dehan Meng <demeng@redhat.com>
On Tue, Apr 23, 2024 at 11:03 PM <aidan_leuck@selinc.com> wrote:
> From: aidaleuc <aidan_leuck@selinc.com>
>
> Signed-off-by: aidaleuc <aidan_leuck@selinc.com>
> ---
> qga/commands-windows-ssh.c | 712 +++++++++++++++++++++++++++++++++++++
> qga/commands-windows-ssh.h | 26 ++
> qga/meson.build | 5 +-
> qga/qapi-schema.json | 17 +-
> 4 files changed, 749 insertions(+), 11 deletions(-)
> create mode 100644 qga/commands-windows-ssh.c
> create mode 100644 qga/commands-windows-ssh.h
>
> diff --git a/qga/commands-windows-ssh.c b/qga/commands-windows-ssh.c
> new file mode 100644
> index 0000000000..6a642e3ba8
> --- /dev/null
> +++ b/qga/commands-windows-ssh.c
> @@ -0,0 +1,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);
> +}
> diff --git a/qga/commands-windows-ssh.h b/qga/commands-windows-ssh.h
> new file mode 100644
> index 0000000000..40ac67c4d9
> --- /dev/null
> +++ b/qga/commands-windows-ssh.h
> @@ -0,0 +1,26 @@
> +/*
> + * Header file for commands-windows-ssh.c
> + *
> + * 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 <glib/gstrfuncs.h>
> +#include <stdbool.h>
> +typedef struct WindowsUserInfo {
> + char *sshDirectory;
> + char *authorizedKeyFile;
> + char *username;
> + char *SSID;
> + bool isAdmin;
> +} WindowsUserInfo;
> +
> +typedef WindowsUserInfo *PWindowsUserInfo;
> +
> +void free_userInfo(PWindowsUserInfo info);
> +G_DEFINE_AUTO_CLEANUP_FREE_FUNC(PWindowsUserInfo, free_userInfo, NULL);
> diff --git a/qga/meson.build b/qga/meson.build
> index 4c3899751b..d5de492da3 100644
> --- a/qga/meson.build
> +++ b/qga/meson.build
> @@ -73,7 +73,8 @@ if host_os == 'windows'
> 'channel-win32.c',
> 'commands-win32.c',
> 'service-win32.c',
> - 'vss-win32.c'
> + 'vss-win32.c',
> + 'commands-windows-ssh.c'
> ))
> else
> qga_ss.add(files(
> @@ -94,7 +95,7 @@ gen_tlb = []
> qga_libs = []
> if host_os == 'windows'
> qga_libs += ['-lws2_32', '-lwinmm', '-lpowrprof', '-lwtsapi32',
> '-lwininet', '-liphlpapi', '-lnetapi32',
> - '-lsetupapi', '-lcfgmgr32']
> + '-lsetupapi', '-lcfgmgr32', '-luserenv']
> if have_qga_vss
> qga_libs += ['-lole32', '-loleaut32', '-lshlwapi', '-lstdc++',
> '-Wl,--enable-stdcall-fixup']
> subdir('vss-win32')
> diff --git a/qga/qapi-schema.json b/qga/qapi-schema.json
> index 9554b566a7..a64a6d91cf 100644
> --- a/qga/qapi-schema.json
> +++ b/qga/qapi-schema.json
> @@ -1562,9 +1562,8 @@
> { 'struct': 'GuestAuthorizedKeys',
> 'data': {
> 'keys': ['str']
> - },
> - 'if': 'CONFIG_POSIX' }
> -
> + }
> +}
>
> ##
> # @guest-ssh-get-authorized-keys:
> @@ -1580,8 +1579,8 @@
> ##
> { 'command': 'guest-ssh-get-authorized-keys',
> 'data': { 'username': 'str' },
> - 'returns': 'GuestAuthorizedKeys',
> - 'if': 'CONFIG_POSIX' }
> + 'returns': 'GuestAuthorizedKeys'
> +}
>
> ##
> # @guest-ssh-add-authorized-keys:
> @@ -1599,8 +1598,8 @@
> # Since: 5.2
> ##
> { 'command': 'guest-ssh-add-authorized-keys',
> - 'data': { 'username': 'str', 'keys': ['str'], '*reset': 'bool' },
> - 'if': 'CONFIG_POSIX' }
> + 'data': { 'username': 'str', 'keys': ['str'], '*reset': 'bool' }
> +}
>
> ##
> # @guest-ssh-remove-authorized-keys:
> @@ -1617,8 +1616,8 @@
> # Since: 5.2
> ##
> { 'command': 'guest-ssh-remove-authorized-keys',
> - 'data': { 'username': 'str', 'keys': ['str'] },
> - 'if': 'CONFIG_POSIX' }
> + 'data': { 'username': 'str', 'keys': ['str'] }
> +}
>
> ##
> # @GuestDiskStats:
> --
> 2.34.1
>
>
>
[-- Attachment #2: Type: text/html, Size: 33930 bytes --]
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v8 2/2] Implement SSH commands in QEMU GA for Windows
2024-04-23 15:00 ` [PATCH v8 2/2] Implement SSH commands in QEMU GA for Windows aidan_leuck
2024-04-24 8:55 ` Dehan Meng
@ 2024-04-24 9:21 ` Dehan Meng
2024-04-24 9:27 ` Konstantin Kostiuk
2 siblings, 0 replies; 8+ messages in thread
From: Dehan Meng @ 2024-04-24 9:21 UTC (permalink / raw)
To: aidan_leuck, qemu-devel; +Cc: Konstantin Kostiuk
[-- Attachment #1: Type: text/plain, Size: 28999 bytes --]
Dehan Meng <demeng@redhat.com>
4:55 PM (15 minutes ago)
to aidaleuc, qemu-devel, kkostiuk
QE tested this series's patches. SSH-relevant commands work and
password-free login can be achieved.
Tested-by: Dehan Meng <demeng@redhat.com>
On Tue, Apr 23, 2024 at 11:03 PM <aidan_leuck@selinc.com> wrote:
> From: aidaleuc <aidan_leuck@selinc.com>
>
> Signed-off-by: aidaleuc <aidan_leuck@selinc.com>
> ---
> qga/commands-windows-ssh.c | 712 +++++++++++++++++++++++++++++++++++++
> qga/commands-windows-ssh.h | 26 ++
> qga/meson.build | 5 +-
> qga/qapi-schema.json | 17 +-
> 4 files changed, 749 insertions(+), 11 deletions(-)
> create mode 100644 qga/commands-windows-ssh.c
> create mode 100644 qga/commands-windows-ssh.h
>
> diff --git a/qga/commands-windows-ssh.c b/qga/commands-windows-ssh.c
> new file mode 100644
> index 0000000000..6a642e3ba8
> --- /dev/null
> +++ b/qga/commands-windows-ssh.c
> @@ -0,0 +1,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);
> +}
> diff --git a/qga/commands-windows-ssh.h b/qga/commands-windows-ssh.h
> new file mode 100644
> index 0000000000..40ac67c4d9
> --- /dev/null
> +++ b/qga/commands-windows-ssh.h
> @@ -0,0 +1,26 @@
> +/*
> + * Header file for commands-windows-ssh.c
> + *
> + * 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 <glib/gstrfuncs.h>
> +#include <stdbool.h>
> +typedef struct WindowsUserInfo {
> + char *sshDirectory;
> + char *authorizedKeyFile;
> + char *username;
> + char *SSID;
> + bool isAdmin;
> +} WindowsUserInfo;
> +
> +typedef WindowsUserInfo *PWindowsUserInfo;
> +
> +void free_userInfo(PWindowsUserInfo info);
> +G_DEFINE_AUTO_CLEANUP_FREE_FUNC(PWindowsUserInfo, free_userInfo, NULL);
> diff --git a/qga/meson.build b/qga/meson.build
> index 4c3899751b..d5de492da3 100644
> --- a/qga/meson.build
> +++ b/qga/meson.build
> @@ -73,7 +73,8 @@ if host_os == 'windows'
> 'channel-win32.c',
> 'commands-win32.c',
> 'service-win32.c',
> - 'vss-win32.c'
> + 'vss-win32.c',
> + 'commands-windows-ssh.c'
> ))
> else
> qga_ss.add(files(
> @@ -94,7 +95,7 @@ gen_tlb = []
> qga_libs = []
> if host_os == 'windows'
> qga_libs += ['-lws2_32', '-lwinmm', '-lpowrprof', '-lwtsapi32',
> '-lwininet', '-liphlpapi', '-lnetapi32',
> - '-lsetupapi', '-lcfgmgr32']
> + '-lsetupapi', '-lcfgmgr32', '-luserenv']
> if have_qga_vss
> qga_libs += ['-lole32', '-loleaut32', '-lshlwapi', '-lstdc++',
> '-Wl,--enable-stdcall-fixup']
> subdir('vss-win32')
> diff --git a/qga/qapi-schema.json b/qga/qapi-schema.json
> index 9554b566a7..a64a6d91cf 100644
> --- a/qga/qapi-schema.json
> +++ b/qga/qapi-schema.json
> @@ -1562,9 +1562,8 @@
> { 'struct': 'GuestAuthorizedKeys',
> 'data': {
> 'keys': ['str']
> - },
> - 'if': 'CONFIG_POSIX' }
> -
> + }
> +}
>
> ##
> # @guest-ssh-get-authorized-keys:
> @@ -1580,8 +1579,8 @@
> ##
> { 'command': 'guest-ssh-get-authorized-keys',
> 'data': { 'username': 'str' },
> - 'returns': 'GuestAuthorizedKeys',
> - 'if': 'CONFIG_POSIX' }
> + 'returns': 'GuestAuthorizedKeys'
> +}
>
> ##
> # @guest-ssh-add-authorized-keys:
> @@ -1599,8 +1598,8 @@
> # Since: 5.2
> ##
> { 'command': 'guest-ssh-add-authorized-keys',
> - 'data': { 'username': 'str', 'keys': ['str'], '*reset': 'bool' },
> - 'if': 'CONFIG_POSIX' }
> + 'data': { 'username': 'str', 'keys': ['str'], '*reset': 'bool' }
> +}
>
> ##
> # @guest-ssh-remove-authorized-keys:
> @@ -1617,8 +1616,8 @@
> # Since: 5.2
> ##
> { 'command': 'guest-ssh-remove-authorized-keys',
> - 'data': { 'username': 'str', 'keys': ['str'] },
> - 'if': 'CONFIG_POSIX' }
> + 'data': { 'username': 'str', 'keys': ['str'] }
> +}
>
> ##
> # @GuestDiskStats:
> --
> 2.34.1
>
>
>
[-- Attachment #2: Type: text/html, Size: 34058 bytes --]
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v8 2/2] Implement SSH commands in QEMU GA for Windows
2024-04-23 15:00 ` [PATCH v8 2/2] Implement SSH commands in QEMU GA for Windows aidan_leuck
2024-04-24 8:55 ` Dehan Meng
2024-04-24 9:21 ` Dehan Meng
@ 2024-04-24 9:27 ` Konstantin Kostiuk
2024-04-24 14:30 ` Aidan Leuck
2 siblings, 1 reply; 8+ messages in thread
From: Konstantin Kostiuk @ 2024-04-24 9:27 UTC (permalink / raw)
To: aidan_leuck; +Cc: qemu-devel
[-- Attachment #1: Type: text/plain, Size: 28889 bytes --]
On Tue, Apr 23, 2024 at 6:02 PM <aidan_leuck@selinc.com> wrote:
> From: aidaleuc <aidan_leuck@selinc.com>
>
> Signed-off-by: aidaleuc <aidan_leuck@selinc.com>
>
Please resend with the real name in Signed-off-by
Assuming this changes:
Reviewed-by: Konstantin Kostiuk <kkostiuk@redhat.com>
> ---
> qga/commands-windows-ssh.c | 712 +++++++++++++++++++++++++++++++++++++
> qga/commands-windows-ssh.h | 26 ++
> qga/meson.build | 5 +-
> qga/qapi-schema.json | 17 +-
> 4 files changed, 749 insertions(+), 11 deletions(-)
> create mode 100644 qga/commands-windows-ssh.c
> create mode 100644 qga/commands-windows-ssh.h
>
> diff --git a/qga/commands-windows-ssh.c b/qga/commands-windows-ssh.c
> new file mode 100644
> index 0000000000..6a642e3ba8
> --- /dev/null
> +++ b/qga/commands-windows-ssh.c
> @@ -0,0 +1,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);
> +}
> diff --git a/qga/commands-windows-ssh.h b/qga/commands-windows-ssh.h
> new file mode 100644
> index 0000000000..40ac67c4d9
> --- /dev/null
> +++ b/qga/commands-windows-ssh.h
> @@ -0,0 +1,26 @@
> +/*
> + * Header file for commands-windows-ssh.c
> + *
> + * 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 <glib/gstrfuncs.h>
> +#include <stdbool.h>
> +typedef struct WindowsUserInfo {
> + char *sshDirectory;
> + char *authorizedKeyFile;
> + char *username;
> + char *SSID;
> + bool isAdmin;
> +} WindowsUserInfo;
> +
> +typedef WindowsUserInfo *PWindowsUserInfo;
> +
> +void free_userInfo(PWindowsUserInfo info);
> +G_DEFINE_AUTO_CLEANUP_FREE_FUNC(PWindowsUserInfo, free_userInfo, NULL);
> diff --git a/qga/meson.build b/qga/meson.build
> index 4c3899751b..d5de492da3 100644
> --- a/qga/meson.build
> +++ b/qga/meson.build
> @@ -73,7 +73,8 @@ if host_os == 'windows'
> 'channel-win32.c',
> 'commands-win32.c',
> 'service-win32.c',
> - 'vss-win32.c'
> + 'vss-win32.c',
> + 'commands-windows-ssh.c'
> ))
> else
> qga_ss.add(files(
> @@ -94,7 +95,7 @@ gen_tlb = []
> qga_libs = []
> if host_os == 'windows'
> qga_libs += ['-lws2_32', '-lwinmm', '-lpowrprof', '-lwtsapi32',
> '-lwininet', '-liphlpapi', '-lnetapi32',
> - '-lsetupapi', '-lcfgmgr32']
> + '-lsetupapi', '-lcfgmgr32', '-luserenv']
> if have_qga_vss
> qga_libs += ['-lole32', '-loleaut32', '-lshlwapi', '-lstdc++',
> '-Wl,--enable-stdcall-fixup']
> subdir('vss-win32')
> diff --git a/qga/qapi-schema.json b/qga/qapi-schema.json
> index 9554b566a7..a64a6d91cf 100644
> --- a/qga/qapi-schema.json
> +++ b/qga/qapi-schema.json
> @@ -1562,9 +1562,8 @@
> { 'struct': 'GuestAuthorizedKeys',
> 'data': {
> 'keys': ['str']
> - },
> - 'if': 'CONFIG_POSIX' }
> -
> + }
> +}
>
> ##
> # @guest-ssh-get-authorized-keys:
> @@ -1580,8 +1579,8 @@
> ##
> { 'command': 'guest-ssh-get-authorized-keys',
> 'data': { 'username': 'str' },
> - 'returns': 'GuestAuthorizedKeys',
> - 'if': 'CONFIG_POSIX' }
> + 'returns': 'GuestAuthorizedKeys'
> +}
>
> ##
> # @guest-ssh-add-authorized-keys:
> @@ -1599,8 +1598,8 @@
> # Since: 5.2
> ##
> { 'command': 'guest-ssh-add-authorized-keys',
> - 'data': { 'username': 'str', 'keys': ['str'], '*reset': 'bool' },
> - 'if': 'CONFIG_POSIX' }
> + 'data': { 'username': 'str', 'keys': ['str'], '*reset': 'bool' }
> +}
>
> ##
> # @guest-ssh-remove-authorized-keys:
> @@ -1617,8 +1616,8 @@
> # Since: 5.2
> ##
> { 'command': 'guest-ssh-remove-authorized-keys',
> - 'data': { 'username': 'str', 'keys': ['str'] },
> - 'if': 'CONFIG_POSIX' }
> + 'data': { 'username': 'str', 'keys': ['str'] }
> +}
>
> ##
> # @GuestDiskStats:
> --
> 2.34.1
>
>
[-- Attachment #2: Type: text/html, Size: 34110 bytes --]
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v8 1/2] Refactor common functions between POSIX and Windows implementation
2024-04-23 15:00 ` [PATCH v8 1/2] Refactor common functions between POSIX and Windows implementation aidan_leuck
@ 2024-04-24 9:28 ` Konstantin Kostiuk
0 siblings, 0 replies; 8+ messages in thread
From: Konstantin Kostiuk @ 2024-04-24 9:28 UTC (permalink / raw)
To: aidan_leuck; +Cc: qemu-devel
[-- Attachment #1: Type: text/plain, Size: 5204 bytes --]
On Tue, Apr 23, 2024 at 6:01 PM <aidan_leuck@selinc.com> wrote:
> From: aidaleuc <aidan_leuck@selinc.com>
>
> Signed-off-by: aidaleuc <aidan_leuck@selinc.com>
>
Please resend with the real name in Signed-off-by
Assuming this changes:
Reviewed-by: Konstantin Kostiuk <kkostiuk@redhat.com>
> ---
> qga/commands-common-ssh.c | 50 +++++++++++++++++++++++++++++++++++++++
> qga/commands-common-ssh.h | 10 ++++++++
> qga/commands-posix-ssh.c | 47 +-----------------------------------
> qga/meson.build | 1 +
> 4 files changed, 62 insertions(+), 46 deletions(-)
> create mode 100644 qga/commands-common-ssh.c
> create mode 100644 qga/commands-common-ssh.h
>
> diff --git a/qga/commands-common-ssh.c b/qga/commands-common-ssh.c
> new file mode 100644
> index 0000000000..537869fb98
> --- /dev/null
> +++ b/qga/commands-common-ssh.c
> @@ -0,0 +1,50 @@
> +/*
> + * 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 "qapi/error.h"
> +#include "commands-common-ssh.h"
> +
> +GStrv read_authkeys(const char *path, Error **errp)
> +{
> + g_autoptr(GError) err = NULL;
> + g_autofree char *contents = NULL;
> +
> + if (!g_file_get_contents(path, &contents, NULL, &err)) {
> + error_setg(errp, "failed to read '%s': %s", path, err->message);
> + return NULL;
> + }
> +
> + return g_strsplit(contents, "\n", -1);
> +}
> +
> +bool check_openssh_pub_keys(strList *keys, size_t *nkeys, Error **errp)
> +{
> + size_t n = 0;
> + strList *k;
> +
> + for (k = keys; k != NULL; k = k->next) {
> + if (!check_openssh_pub_key(k->value, errp)) {
> + return false;
> + }
> + n++;
> + }
> +
> + if (nkeys) {
> + *nkeys = n;
> + }
> + return true;
> +}
> +
> +bool check_openssh_pub_key(const char *key, Error **errp)
> +{
> + /* simple sanity-check, we may want more? */
> + if (!key || key[0] == '#' || strchr(key, '\n')) {
> + error_setg(errp, "invalid OpenSSH public key: '%s'", key);
> + return false;
> + }
> +
> + return true;
> +}
> diff --git a/qga/commands-common-ssh.h b/qga/commands-common-ssh.h
> new file mode 100644
> index 0000000000..14d955fa84
> --- /dev/null
> +++ b/qga/commands-common-ssh.h
> @@ -0,0 +1,10 @@
> +/*
> + * 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 "qapi/qapi-builtin-types.h"
> +
> +GStrv read_authkeys(const char *path, Error **errp);
> +bool check_openssh_pub_keys(strList *keys, size_t *nkeys, Error **errp);
> +bool check_openssh_pub_key(const char *key, Error **errp);
> diff --git a/qga/commands-posix-ssh.c b/qga/commands-posix-ssh.c
> index 236f80de44..dd2ecb453a 100644
> --- a/qga/commands-posix-ssh.c
> +++ b/qga/commands-posix-ssh.c
> @@ -9,6 +9,7 @@
> #include <locale.h>
> #include <pwd.h>
>
> +#include "commands-common-ssh.h"
> #include "qapi/error.h"
> #include "qga-qapi-commands.h"
>
> @@ -80,37 +81,6 @@ mkdir_for_user(const char *path, const struct passwd *p,
> return true;
> }
>
> -static bool
> -check_openssh_pub_key(const char *key, Error **errp)
> -{
> - /* simple sanity-check, we may want more? */
> - if (!key || key[0] == '#' || strchr(key, '\n')) {
> - error_setg(errp, "invalid OpenSSH public key: '%s'", key);
> - return false;
> - }
> -
> - return true;
> -}
> -
> -static bool
> -check_openssh_pub_keys(strList *keys, size_t *nkeys, Error **errp)
> -{
> - size_t n = 0;
> - strList *k;
> -
> - for (k = keys; k != NULL; k = k->next) {
> - if (!check_openssh_pub_key(k->value, errp)) {
> - return false;
> - }
> - n++;
> - }
> -
> - if (nkeys) {
> - *nkeys = n;
> - }
> - return true;
> -}
> -
> static bool
> write_authkeys(const char *path, const GStrv keys,
> const struct passwd *p, Error **errp)
> @@ -139,21 +109,6 @@ write_authkeys(const char *path, const GStrv keys,
> return true;
> }
>
> -static GStrv
> -read_authkeys(const char *path, Error **errp)
> -{
> - g_autoptr(GError) err = NULL;
> - g_autofree char *contents = NULL;
> -
> - if (!g_file_get_contents(path, &contents, NULL, &err)) {
> - error_setg(errp, "failed to read '%s': %s", path, err->message);
> - return NULL;
> - }
> -
> - return g_strsplit(contents, "\n", -1);
> -
> -}
> -
> void
> qmp_guest_ssh_add_authorized_keys(const char *username, strList *keys,
> bool has_reset, bool reset,
> diff --git a/qga/meson.build b/qga/meson.build
> index 1c3d2a3d1b..4c3899751b 100644
> --- a/qga/meson.build
> +++ b/qga/meson.build
> @@ -66,6 +66,7 @@ qga_ss.add(files(
> 'guest-agent-command-state.c',
> 'main.c',
> 'cutils.c',
> + 'commands-common-ssh.c'
> ))
> if host_os == 'windows'
> qga_ss.add(files(
> --
> 2.34.1
>
>
[-- Attachment #2: Type: text/html, Size: 6705 bytes --]
^ permalink raw reply [flat|nested] 8+ messages in thread
* RE: [PATCH v8 2/2] Implement SSH commands in QEMU GA for Windows
2024-04-24 9:27 ` Konstantin Kostiuk
@ 2024-04-24 14:30 ` Aidan Leuck
0 siblings, 0 replies; 8+ messages in thread
From: Aidan Leuck @ 2024-04-24 14:30 UTC (permalink / raw)
To: Konstantin Kostiuk; +Cc: qemu-devel@nongnu.org, Dehan Meng
[-- Attachment #1: Type: text/plain, Size: 27795 bytes --]
Hi Konstantin,
I wanted to thank both you and Dehan for spending the time to review and test this patch. I appreciate it and am happy to hear that everything is working as expected! I will resubmit the patch with the updated signed off field.
Thank you,
Aidan Leuck
From: Konstantin Kostiuk <kkostiuk@redhat.com>
Sent: Wednesday, April 24, 2024 3:28 AM
To: Aidan Leuck <aidan_leuck@selinc.com>
Cc: qemu-devel@nongnu.org
Subject: Re: [PATCH v8 2/2] Implement SSH commands in QEMU GA for Windows
[Caution - External]
On Tue, Apr 23, 2024 at 6:02 PM <aidan_leuck@selinc.com<mailto:aidan_leuck@selinc.com>> wrote:
From: aidaleuc <aidan_leuck@selinc.com<mailto:aidan_leuck@selinc.com>>
Signed-off-by: aidaleuc <aidan_leuck@selinc.com<mailto:aidan_leuck@selinc.com>>
Please resend with the real name in Signed-off-by
Assuming this changes:
Reviewed-by: Konstantin Kostiuk <kkostiuk@redhat.com<mailto:kkostiuk@redhat.com>>
---
qga/commands-windows-ssh.c | 712 +++++++++++++++++++++++++++++++++++++
qga/commands-windows-ssh.h | 26 ++
qga/meson.build | 5 +-
qga/qapi-schema.json | 17 +-
4 files changed, 749 insertions(+), 11 deletions(-)
create mode 100644 qga/commands-windows-ssh.c
create mode 100644 qga/commands-windows-ssh.h
diff --git a/qga/commands-windows-ssh.c b/qga/commands-windows-ssh.c
new file mode 100644
index 0000000000..6a642e3ba8
--- /dev/null
+++ b/qga/commands-windows-ssh.c
@@ -0,0 +1,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<mailto: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);
+}
diff --git a/qga/commands-windows-ssh.h b/qga/commands-windows-ssh.h
new file mode 100644
index 0000000000..40ac67c4d9
--- /dev/null
+++ b/qga/commands-windows-ssh.h
@@ -0,0 +1,26 @@
+/*
+ * Header file for commands-windows-ssh.c
+ *
+ * Copyright Schweitzer Engineering Laboratories. 2024
+ *
+ * Authors:
+ * Aidan Leuck <aidan_leuck@selinc.com<mailto: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 <glib/gstrfuncs.h>
+#include <stdbool.h>
+typedef struct WindowsUserInfo {
+ char *sshDirectory;
+ char *authorizedKeyFile;
+ char *username;
+ char *SSID;
+ bool isAdmin;
+} WindowsUserInfo;
+
+typedef WindowsUserInfo *PWindowsUserInfo;
+
+void free_userInfo(PWindowsUserInfo info);
+G_DEFINE_AUTO_CLEANUP_FREE_FUNC(PWindowsUserInfo, free_userInfo, NULL);
diff --git a/qga/meson.build b/qga/meson.build
index 4c3899751b..d5de492da3 100644
--- a/qga/meson.build
+++ b/qga/meson.build
@@ -73,7 +73,8 @@ if host_os == 'windows'
'channel-win32.c',
'commands-win32.c',
'service-win32.c',
- 'vss-win32.c'
+ 'vss-win32.c',
+ 'commands-windows-ssh.c'
))
else
qga_ss.add(files(
@@ -94,7 +95,7 @@ gen_tlb = []
qga_libs = []
if host_os == 'windows'
qga_libs += ['-lws2_32', '-lwinmm', '-lpowrprof', '-lwtsapi32', '-lwininet', '-liphlpapi', '-lnetapi32',
- '-lsetupapi', '-lcfgmgr32']
+ '-lsetupapi', '-lcfgmgr32', '-luserenv']
if have_qga_vss
qga_libs += ['-lole32', '-loleaut32', '-lshlwapi', '-lstdc++', '-Wl,--enable-stdcall-fixup']
subdir('vss-win32')
diff --git a/qga/qapi-schema.json b/qga/qapi-schema.json
index 9554b566a7..a64a6d91cf 100644
--- a/qga/qapi-schema.json
+++ b/qga/qapi-schema.json
@@ -1562,9 +1562,8 @@
{ 'struct': 'GuestAuthorizedKeys',
'data': {
'keys': ['str']
- },
- 'if': 'CONFIG_POSIX' }
-
+ }
+}
##
# @guest-ssh-get-authorized-keys:
@@ -1580,8 +1579,8 @@
##
{ 'command': 'guest-ssh-get-authorized-keys',
'data': { 'username': 'str' },
- 'returns': 'GuestAuthorizedKeys',
- 'if': 'CONFIG_POSIX' }
+ 'returns': 'GuestAuthorizedKeys'
+}
##
# @guest-ssh-add-authorized-keys:
@@ -1599,8 +1598,8 @@
# Since: 5.2
##
{ 'command': 'guest-ssh-add-authorized-keys',
- 'data': { 'username': 'str', 'keys': ['str'], '*reset': 'bool' },
- 'if': 'CONFIG_POSIX' }
+ 'data': { 'username': 'str', 'keys': ['str'], '*reset': 'bool' }
+}
##
# @guest-ssh-remove-authorized-keys:
@@ -1617,8 +1616,8 @@
# Since: 5.2
##
{ 'command': 'guest-ssh-remove-authorized-keys',
- 'data': { 'username': 'str', 'keys': ['str'] },
- 'if': 'CONFIG_POSIX' }
+ 'data': { 'username': 'str', 'keys': ['str'] }
+}
##
# @GuestDiskStats:
--
2.34.1
[-- Attachment #2: Type: text/html, Size: 43493 bytes --]
^ permalink raw reply related [flat|nested] 8+ messages in thread
end of thread, other threads:[~2024-04-24 14:31 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2024-04-23 15:00 [PATCH v8 0/2] Implement SSH commands in QEMU GA for Windows aidan_leuck
2024-04-23 15:00 ` [PATCH v8 1/2] Refactor common functions between POSIX and Windows implementation aidan_leuck
2024-04-24 9:28 ` Konstantin Kostiuk
2024-04-23 15:00 ` [PATCH v8 2/2] Implement SSH commands in QEMU GA for Windows aidan_leuck
2024-04-24 8:55 ` Dehan Meng
2024-04-24 9:21 ` Dehan Meng
2024-04-24 9:27 ` Konstantin Kostiuk
2024-04-24 14:30 ` Aidan Leuck
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).