/* 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 "libnm-core-intern/nm-keyfile-internal.h"
#include "nms-keyfile-utils.h"
#include "nms-keyfile-reader.h"
#include "libnm-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 blobs 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,
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, 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);
}