diff options
Diffstat (limited to 'src/core/settings/plugins/keyfile/nms-keyfile-writer.c')
| -rw-r--r-- | src/core/settings/plugins/keyfile/nms-keyfile-writer.c | 489 |
1 files changed, 489 insertions, 0 deletions
diff --git a/src/core/settings/plugins/keyfile/nms-keyfile-writer.c b/src/core/settings/plugins/keyfile/nms-keyfile-writer.c new file mode 100644 index 00000000..0e2c94b5 --- /dev/null +++ b/src/core/settings/plugins/keyfile/nms-keyfile-writer.c @@ -0,0 +1,489 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2008 Novell, Inc. + * Copyright (C) 2008 - 2015 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nms-keyfile-writer.h" + +#include <stdlib.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "nm-keyfile-internal.h" + +#include "nms-keyfile-utils.h" +#include "nms-keyfile-reader.h" + +#include "nm-glib-aux/nm-io-utils.h" + +/*****************************************************************************/ + +typedef struct { + const char *keyfile_dir; +} WriteInfo; + +static void +cert_writer(NMConnection * connection, + GKeyFile * file, + NMSetting8021x * setting, + const NMSetting8021xSchemeVtable *vtable, + WriteInfo * info, + GError ** error) +{ + const char * setting_name = nm_setting_get_name(NM_SETTING(setting)); + NMSetting8021xCKScheme scheme; + NMSetting8021xCKFormat format; + const char * path = NULL, *ext = "pem"; + + scheme = vtable->scheme_func(setting); + if (scheme == NM_SETTING_802_1X_CK_SCHEME_PATH) { + char * tmp = NULL; + const char *accepted_path = NULL; + + path = vtable->path_func(setting); + g_assert(path); + + if (g_str_has_prefix(path, info->keyfile_dir)) { + const char *p = path + strlen(info->keyfile_dir); + + /* If the path is rooted in the keyfile directory, just use a + * relative path instead of an absolute one. + */ + if (*p == '/') { + while (*p == '/') + p++; + if (p[0]) { + /* If @p looks like an integer list, the following detection will fail too and + * we will file:// qualify the path below. We thus avoid writing a path string + * that would be interpreted as legacy binary format by reader. */ + tmp = nm_keyfile_detect_unqualified_path_scheme(info->keyfile_dir, + p, + -1, + FALSE, + NULL); + if (tmp) { + nm_clear_g_free(&tmp); + accepted_path = p; + } + } + } + } + if (!accepted_path) { + /* What we are about to write, must also be understood by the reader. + * Otherwise, add a file:// prefix */ + tmp = + nm_keyfile_detect_unqualified_path_scheme(info->keyfile_dir, path, -1, FALSE, NULL); + if (tmp) { + nm_clear_g_free(&tmp); + accepted_path = path; + } + } + + if (!accepted_path) + accepted_path = tmp = g_strconcat(NM_KEYFILE_CERT_SCHEME_PREFIX_PATH, path, NULL); + nm_keyfile_plugin_kf_set_string(file, setting_name, vtable->setting_key, accepted_path); + g_free(tmp); + } else if (scheme == NM_SETTING_802_1X_CK_SCHEME_PKCS11) { + nm_keyfile_plugin_kf_set_string(file, + setting_name, + vtable->setting_key, + vtable->uri_func(setting)); + } else if (scheme == NM_SETTING_802_1X_CK_SCHEME_BLOB) { + GBytes * blob; + const guint8 *blob_data; + gsize blob_len; + gboolean success; + GError * local = NULL; + char * new_path; + + blob = vtable->blob_func(setting); + g_assert(blob); + blob_data = g_bytes_get_data(blob, &blob_len); + + if (vtable->format_func) { + /* Get the extension for a private key */ + format = vtable->format_func(setting); + if (format == NM_SETTING_802_1X_CK_FORMAT_PKCS12) + ext = "p12"; + } else { + /* DER or PEM format certificate? */ + if (blob_len > 2 && blob_data[0] == 0x30 && blob_data[1] == 0x82) + ext = "der"; + } + + /* Write the raw data out to the standard file so that we can use paths + * from now on instead of pushing around the certificate data. + */ + new_path = g_strdup_printf("%s/%s-%s.%s", + info->keyfile_dir, + nm_connection_get_uuid(connection), + vtable->file_suffix, + ext); + + /* FIXME(keyfile-parse-in-memory): writer must not access/write to the file system before + * being sure that the entire profile can be written and all circumstances are good to + * proceed. That means, while writing we must only collect the blogs in-memory, and write + * them all in the end together (or not at all). */ + success = nm_utils_file_set_contents(new_path, + (const char *) blob_data, + blob_len, + 0600, + NULL, + &local); + if (success) { + /* Write the path value to the keyfile. + * We know, that basename(new_path) starts with a UUID, hence no conflict with "data:;base64," */ + nm_keyfile_plugin_kf_set_string(file, + setting_name, + vtable->setting_key, + strrchr(new_path, '/') + 1); + } else { + nm_log_warn(LOGD_SETTINGS, + "keyfile: %s.%s: failed to write certificate to file %s: %s", + setting_name, + vtable->setting_key, + new_path, + local->message); + g_error_free(local); + } + g_free(new_path); + } else { + /* scheme_func() returns UNKNOWN in all other cases. The only valid case + * where a scheme is allowed to be UNKNOWN, is unsetting the value. In this + * case, we don't expect the writer to be called, because the default value + * will not be serialized. + * The only other reason for the scheme to be UNKNOWN is an invalid cert. + * But our connection verifies, so that cannot happen either. */ + g_return_if_reached(); + } +} + +static gboolean +_handler_write(NMConnection * connection, + GKeyFile * keyfile, + NMKeyfileHandlerType type, + NMKeyfileHandlerData *type_data, + void * user_data) +{ + if (type == NM_KEYFILE_HANDLER_TYPE_WRITE_CERT) { + cert_writer(connection, + keyfile, + NM_SETTING_802_1X(type_data->cur_setting), + type_data->write_cert.vtable, + user_data, + type_data->p_error); + return TRUE; + } + return FALSE; +} + +static gboolean +_internal_write_connection(NMConnection * connection, + gboolean is_nm_generated, + gboolean is_volatile, + gboolean is_external, + const char * shadowed_storage, + gboolean shadowed_owned, + const char * keyfile_dir, + const char * profile_dir, + gboolean with_extension, + uid_t owner_uid, + pid_t owner_grp, + const char * existing_path, + gboolean existing_path_read_only, + gboolean force_rename, + NMSKeyfileWriterAllowFilenameCb allow_filename_cb, + gpointer allow_filename_user_data, + char ** out_path, + NMConnection ** out_reread, + gboolean * out_reread_same, + GError ** error) +{ + nm_auto_unref_keyfile GKeyFile *kf_file = NULL; + gs_free char * kf_content_buf = NULL; + gsize kf_content_len; + gs_free char * path = NULL; + const char * id; + WriteInfo info = {0}; + gs_free_error GError *local_err = NULL; + int errsv; + gboolean rename; + int i_path; + gs_unref_object NMConnection *reread = NULL; + gboolean reread_same = FALSE; + + g_return_val_if_fail(!out_path || !*out_path, FALSE); + g_return_val_if_fail(keyfile_dir && keyfile_dir[0] == '/', FALSE); + + nm_assert(_nm_connection_verify(connection, NULL) == NM_SETTING_VERIFY_SUCCESS); + + nm_assert(!shadowed_owned || shadowed_storage); + + rename = force_rename || existing_path_read_only + || (existing_path && !nm_utils_file_is_in_path(existing_path, keyfile_dir)); + + id = nm_connection_get_id(connection); + nm_assert(id && *id); + + info.keyfile_dir = keyfile_dir; + + kf_file = + nm_keyfile_write(connection, NM_KEYFILE_HANDLER_FLAGS_NONE, _handler_write, &info, error); + if (!kf_file) + return FALSE; + + if (is_nm_generated) { + g_key_file_set_boolean(kf_file, + NM_KEYFILE_GROUP_NMMETA, + NM_KEYFILE_KEY_NMMETA_NM_GENERATED, + TRUE); + } + + if (is_volatile) { + g_key_file_set_boolean(kf_file, + NM_KEYFILE_GROUP_NMMETA, + NM_KEYFILE_KEY_NMMETA_VOLATILE, + TRUE); + } + + if (is_external) { + g_key_file_set_boolean(kf_file, + NM_KEYFILE_GROUP_NMMETA, + NM_KEYFILE_KEY_NMMETA_EXTERNAL, + TRUE); + } + + if (shadowed_storage) { + g_key_file_set_string(kf_file, + NM_KEYFILE_GROUP_NMMETA, + NM_KEYFILE_KEY_NMMETA_SHADOWED_STORAGE, + shadowed_storage); + } + + if (shadowed_owned) { + g_key_file_set_boolean(kf_file, + NM_KEYFILE_GROUP_NMMETA, + NM_KEYFILE_KEY_NMMETA_SHADOWED_OWNED, + TRUE); + } + + kf_content_buf = g_key_file_to_data(kf_file, &kf_content_len, error); + if (!kf_content_buf) + return FALSE; + + if (!g_file_test(keyfile_dir, G_FILE_TEST_IS_DIR)) + (void) g_mkdir_with_parents(keyfile_dir, 0755); + + for (i_path = -2; i_path < 10000; i_path++) { + gs_free char *path_candidate = NULL; + gboolean is_existing_path; + + if (i_path == -2) { + if (!existing_path || rename) + continue; + path_candidate = g_strdup(existing_path); + } else if (i_path == -1) { + gs_free char *filename_escaped = NULL; + + filename_escaped = nm_keyfile_utils_create_filename(id, with_extension); + path_candidate = g_build_filename(keyfile_dir, filename_escaped, NULL); + } else { + gs_free char *filename_escaped = NULL; + gs_free char *filename = NULL; + + if (i_path == 0) + filename = g_strdup_printf("%s-%s", id, nm_connection_get_uuid(connection)); + else + filename = + g_strdup_printf("%s-%s-%d", id, nm_connection_get_uuid(connection), i_path); + + filename_escaped = nm_keyfile_utils_create_filename(filename, with_extension); + + path_candidate = g_strdup_printf("%s/%s", keyfile_dir, filename_escaped); + } + + is_existing_path = existing_path && nm_streq(existing_path, path_candidate); + + if (is_existing_path && rename) + continue; + + if (allow_filename_cb && !allow_filename_cb(path_candidate, allow_filename_user_data)) + continue; + + if (!is_existing_path) { + if (g_file_test(path_candidate, G_FILE_TEST_EXISTS)) + continue; + } + + path = g_steal_pointer(&path_candidate); + break; + } + + if (!path) { + gs_free char *ss = NULL; + + /* this really should not happen, we tried hard to find an unused name... bail out. */ + g_set_error(error, + NM_SETTINGS_ERROR, + NM_SETTINGS_ERROR_FAILED, + "could not find suitable keyfile file name (%s already used)", + ss = ({ + gs_free char *filename_escaped = NULL; + + filename_escaped = nm_keyfile_utils_create_filename(id, with_extension); + g_build_filename(keyfile_dir, filename_escaped, NULL); + })); + + return FALSE; + } + + if (out_reread || out_reread_same) { + gs_free_error GError *reread_error = NULL; + + reread = + nms_keyfile_reader_from_keyfile(kf_file, path, NULL, profile_dir, FALSE, &reread_error); + + if (!reread || !nm_connection_normalize(reread, NULL, NULL, &reread_error)) { + nm_log_err( + LOGD_SETTINGS, + "BUG: the profile cannot be stored in keyfile format without becoming unusable: %s", + reread_error->message); + g_set_error(error, + NM_SETTINGS_ERROR, + NM_SETTINGS_ERROR_FAILED, + "keyfile writer produces an invalid connection: %s", + reread_error->message); + nm_assert_not_reached(); + return FALSE; + } + + if (out_reread_same) { + reread_same = + !!nm_connection_compare(reread, connection, NM_SETTING_COMPARE_FLAG_EXACT); + + nm_assert(reread_same + == nm_connection_compare(connection, reread, NM_SETTING_COMPARE_FLAG_EXACT)); + nm_assert(reread_same == ({ + gs_unref_hashtable GHashTable *_settings = NULL; + + (nm_connection_diff(reread, + connection, + NM_SETTING_COMPARE_FLAG_EXACT, + &_settings) + && !_settings); + })); + } + } + + nm_utils_file_set_contents(path, kf_content_buf, kf_content_len, 0600, NULL, &local_err); + if (local_err) { + g_set_error(error, + NM_SETTINGS_ERROR, + NM_SETTINGS_ERROR_FAILED, + "error writing to file '%s': %s", + path, + local_err->message); + return FALSE; + } + + if (chown(path, owner_uid, owner_grp) < 0) { + errsv = errno; + g_set_error(error, + NM_SETTINGS_ERROR, + NM_SETTINGS_ERROR_FAILED, + "error chowning '%s': %s (%d)", + path, + nm_strerror_native(errsv), + errsv); + unlink(path); + return FALSE; + } + + /* In case of updating the connection and changing the file path, + * we need to remove the old one, not to end up with two connections. + */ + if (existing_path && !existing_path_read_only && !nm_streq(path, existing_path)) + unlink(existing_path); + + NM_SET_OUT(out_reread, g_steal_pointer(&reread)); + NM_SET_OUT(out_reread_same, reread_same); + NM_SET_OUT(out_path, g_steal_pointer(&path)); + + return TRUE; +} + +gboolean +nms_keyfile_writer_connection(NMConnection * connection, + gboolean is_nm_generated, + gboolean is_volatile, + gboolean is_external, + const char * shadowed_storage, + gboolean shadowed_owned, + const char * keyfile_dir, + const char * profile_dir, + const char * existing_path, + gboolean existing_path_read_only, + gboolean force_rename, + NMSKeyfileWriterAllowFilenameCb allow_filename_cb, + gpointer allow_filename_user_data, + char ** out_path, + NMConnection ** out_reread, + gboolean * out_reread_same, + GError ** error) +{ + return _internal_write_connection(connection, + is_nm_generated, + is_volatile, + is_external, + shadowed_storage, + shadowed_owned, + keyfile_dir, + profile_dir, + TRUE, + 0, + 0, + existing_path, + existing_path_read_only, + force_rename, + allow_filename_cb, + allow_filename_user_data, + out_path, + out_reread, + out_reread_same, + error); +} + +gboolean +nms_keyfile_writer_test_connection(NMConnection * connection, + const char * keyfile_dir, + uid_t owner_uid, + pid_t owner_grp, + char ** out_path, + NMConnection **out_reread, + gboolean * out_reread_same, + GError ** error) +{ + return _internal_write_connection(connection, + FALSE, + FALSE, + FALSE, + NULL, + FALSE, + keyfile_dir, + keyfile_dir, + FALSE, + owner_uid, + owner_grp, + NULL, + FALSE, + FALSE, + NULL, + NULL, + out_path, + out_reread, + out_reread_same, + error); +} |