// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright (C) 2008 Novell, Inc.
* Copyright (C) 2008 - 2015 Red Hat, Inc.
*/
#include "nm-default.h"
#include "nms-keyfile-writer.h"
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include "nm-keyfile/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)
{
gs_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) {
/* 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)", path);
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);
}