From deb19abf22eca93bdb57f52d162b5d81f1a85fc1 Mon Sep 17 00:00:00 2001 From: Thomas Haller Date: Wed, 5 Dec 2018 13:42:33 +0100 Subject: [PATCH] core: combine secret-key with /etc/machine-id NetworkManager loads (and generates) a secret key as "/var/lib/NetworkManager/secret_key". The secret key is used for seeding a per-host component when generating hashed, stable data. For example, it contributes to "ipv4.dhcp-client-id=duid" "ipv6.addr-gen-mode=stable-privacy", "ethernet.cloned-mac-address=stable", etc. As such, it corresponds to the identity of the host. Also "/etc/machine-id" is the host's identity. When cloning a virtual machine, it may be a good idea to generate a new "/etc/machine-id", at least in those cases where the VM's identity shall be different. Systemd provides various mechanisms for doing that, like accepting a new machine-id via kernel command line. For the same reason, the user should also regenerate a new NetworkManager's secrey key when the host's identity shall change. However, that is less obvious, less understood and less documented. Support and use a new variant of secret key. This secret key is combined with "/etc/machine-id" by sha256 hashing it together. That means, when the user generates a new machine-id, NetworkManager's per-host key also changes. Since we don't want to change behavior for existing installations, we only do this when generating a new secret key file. For that, we encode a version tag inside the "/var/lib/NetworkManager/secret_key" file. Note that this is all abstracted by nm_utils_secret_key_get(). For version 2 secret-keys, it internally combines the secret_key file with machine-id (via sha256). The advantage is that callers don't care that the secret-key now also contains the machine-id. Also, since we want to stick to the previous behavior if we have an old secret-key, this is nicely abstracted. Otherwise, the caller would not only need to handle two per-host parts, but it would also need to check the version to determine whether the machine-id should be explicitly included. At this point, nm_utils_secret_key_get() should be renamed to nm_utils_host_key_get(). --- src/nm-core-utils.c | 181 +++++++++++++++++++++++++++++++++----------- 1 file changed, 136 insertions(+), 45 deletions(-) diff --git a/src/nm-core-utils.c b/src/nm-core-utils.c index 6b7ed172d..e02aaf80f 100644 --- a/src/nm-core-utils.c +++ b/src/nm-core-utils.c @@ -41,6 +41,7 @@ #include "nm-utils/nm-random-utils.h" #include "nm-utils/nm-io-utils.h" #include "nm-utils/unaligned.h" +#include "nm-utils/nm-secret-utils.h" #include "nm-utils.h" #include "nm-core-internal.h" #include "nm-setting-connection.h" @@ -2406,7 +2407,7 @@ _uuid_data_init (UuidData *uuid_data, /*****************************************************************************/ static const UuidData * -_machine_id_get (void) +_machine_id_get (gboolean allow_fake) { static const UuidData *volatile p_uuid_data; const UuidData *d; @@ -2448,6 +2449,12 @@ again: const char *hash_seed; gsize seed_len; + if (!allow_fake) { + /* we don't allow generating (and memoizing) a fake key. + * Signal that no valid machine-id exists. */ + return NULL; + } + if (nm_utils_secret_key_get (&seed_bin, &seed_len)) { /* we have no valid machine-id. Generate a fake one by hashing * the secret-key. This key is commonly persisted, so it should be @@ -2497,84 +2504,168 @@ again: const char * nm_utils_machine_id_str (void) { - return _machine_id_get ()->str; + return _machine_id_get (TRUE)->str; } const NMUuid * nm_utils_machine_id_bin (void) { - return &_machine_id_get ()->bin; + return &_machine_id_get (TRUE)->bin; } gboolean nm_utils_machine_id_is_fake (void) { - return _machine_id_get ()->is_fake; + return _machine_id_get (TRUE)->is_fake; } /*****************************************************************************/ +/* prefix for version2 secret key. The secret key is hashed with /etc/machine-id. */ +#define SECRET_KEY_V2_PREFIX "nm-v2:" #define SECRET_KEY_FILE NMSTATEDIR"/secret_key" +static const guint8 * +_secret_key_hash_v2 (const guint8 *seed_arr, + gsize seed_len, + guint8 *out_digest /* 32 bytes (NM_UTILS_CHECKSUM_LENGTH_SHA256) */) +{ + nm_auto_free_checksum GChecksum *sum = g_checksum_new (G_CHECKSUM_SHA256); + const UuidData *machine_id_data; + char slen[100]; + + /* + (stat -c '%s' /var/lib/NetworkManager/secret_key; + echo -n ' '; + cat /var/lib/NetworkManager/secret_key; + cat /etc/machine-id | tr -d '\n' | sed -n 's/[a-f0-9-]/\0/pg') | sha256sum + */ + + nm_sprintf_buf (slen, "%"G_GSIZE_FORMAT" ", seed_len); + g_checksum_update (sum, (const guchar *) slen, strlen (slen)); + + g_checksum_update (sum, (const guchar *) seed_arr, seed_len); + + machine_id_data = _machine_id_get (FALSE); + if ( machine_id_data + && !machine_id_data->is_fake) + g_checksum_update (sum, (const guchar *) machine_id_data->str, strlen (machine_id_data->str)); + + nm_utils_checksum_get_digest_len (sum, out_digest, NM_UTILS_CHECKSUM_LENGTH_SHA256); + return out_digest; +} + static gboolean -_secret_key_read (guint8 **out_secret_key, +_secret_key_read (guint8 **out_key, gsize *out_key_len) { - guint8 *secret_key; - gboolean success = TRUE; - gsize key_len; - gs_free_error GError *error = NULL; +#define SECRET_KEY_LEN 32u + guint8 sha256_digest[NM_UTILS_CHECKSUM_LENGTH_SHA256]; + nm_auto_clear_secret_ptr NMSecretPtr file_content = { 0 }; + const guint8 *secret_arr; + gsize secret_len; + GError *error = NULL; + gboolean success; - /* Let's try to load a saved secret key first. */ - if (g_file_get_contents (SECRET_KEY_FILE, (char **) &secret_key, &key_len, &error)) { - if (key_len >= 16) - goto out; - - /* the secret key is borked. Log a warning, but proceed below to generate - * a new one. */ - nm_log_warn (LOGD_CORE, "secret-key: too short secret key in \"%s\" (generate new key)", SECRET_KEY_FILE); - nm_clear_g_free (&secret_key); - } else { + if (nm_utils_file_get_contents (-1, + SECRET_KEY_FILE, + 10*1024, + NM_UTILS_FILE_GET_CONTENTS_FLAG_SECRET, + (char **) &file_content.str, + &file_content.len, + &error) < 0) { if (!nm_utils_error_is_notfound (error)) { nm_log_warn (LOGD_CORE, "secret-key: failure reading secret key in \"%s\": %s (generate new key)", SECRET_KEY_FILE, error->message); } g_clear_error (&error); + } else if ( file_content.len >= NM_STRLEN (SECRET_KEY_V2_PREFIX) + SECRET_KEY_LEN + && memcmp (file_content.bin, SECRET_KEY_V2_PREFIX, NM_STRLEN (SECRET_KEY_V2_PREFIX)) == 0) { + /* for this type of secret key, we require a prefix followed at least SECRET_KEY_LEN (32) bytes. We + * (also) do that, because older versions of NetworkManager wrote exactly 32 bytes without + * prefix, so we won't wrongly interpret such legacy keys as v2 (if they accidentally have + * a SECRET_KEY_V2_PREFIX prefix, they'll still have the wrong size). + * + * Note that below we generate the random seed in base64 encoding. But that is only done + * to write an ASCII file. There is no base64 decoding and the ASCII is hashed as-is. + * We would accept any binary data just as well (provided a suitable prefix and at least + * 32 bytes). + * + * Note that when hashing the v2 content, we also hash the prefix. There is no strong reason, + * except that it seems simpler not to distinguish between the v2 prefix and the content. + * It's all just part of the seed. */ + + secret_arr = _secret_key_hash_v2 (file_content.bin, file_content.len, sha256_digest); + secret_len = NM_UTILS_CHECKSUM_LENGTH_SHA256; + success = TRUE; + goto out; + } else if (file_content.len >= 16) { + secret_arr = file_content.bin; + secret_len = file_content.len; + success = TRUE; + goto out; + } else { + /* the secret key is borked. Log a warning, but proceed below to generate + * a new one. */ + nm_log_warn (LOGD_CORE, "secret-key: too short secret key in \"%s\" (generate new key)", SECRET_KEY_FILE); } - /* RFC7217 mandates the key SHOULD be at least 128 bits. - * Let's use twice as much. */ - key_len = 32; - secret_key = g_malloc (key_len + 1); + /* generate and persist new key */ + { +#define SECRET_KEY_LEN_BASE64 ((((SECRET_KEY_LEN / 3) + 1) * 4) + 4) + guint8 rnd_buf[SECRET_KEY_LEN]; + guint8 new_content[NM_STRLEN (SECRET_KEY_V2_PREFIX) + SECRET_KEY_LEN_BASE64]; + int base64_state = 0; + int base64_save = 0; + gsize len; - /* the secret-key is binary. Still, ensure that it's NULL terminated, just like - * g_file_set_contents() does. */ - secret_key[32] = '\0'; + success = nm_utils_random_bytes (rnd_buf, sizeof (rnd_buf)); - if (!nm_utils_random_bytes (secret_key, key_len)) { - nm_log_warn (LOGD_CORE, "secret-key: failure to generate good random data for secret-key (use non-persistent key)"); - success = FALSE; - goto out; - } + /* Our key is really binary data. But since we anyway generate a random seed + * (with 32 random bytes), don't write it in binary, but instead create + * an pure ASCII (base64) representation. Note that the ASCII will still be taken + * as-is (no base64 decoding is done). The sole purpose is to write a ASCII file + * instead of a binary. The content is gibberish either way. */ + memcpy (new_content, SECRET_KEY_V2_PREFIX, NM_STRLEN (SECRET_KEY_V2_PREFIX)); + len = NM_STRLEN (SECRET_KEY_V2_PREFIX); + len += g_base64_encode_step (rnd_buf, + sizeof (rnd_buf), + FALSE, + (char *) &new_content[len], + &base64_state, + &base64_save); + len += g_base64_encode_close (FALSE, + (char *) &new_content[len], + &base64_state, + &base64_save); + nm_assert (len <= sizeof (new_content)); - if (nm_utils_get_testing ()) { - /* for test code, we don't write the generated secret-key to disk. */ - success = FALSE; - goto out; - } + secret_arr = _secret_key_hash_v2 (new_content, len, sha256_digest); + secret_len = NM_UTILS_CHECKSUM_LENGTH_SHA256; - if (!nm_utils_file_set_contents (SECRET_KEY_FILE, (char *) secret_key, key_len, 0077, &error)) { - nm_log_warn (LOGD_CORE, "secret-key: failure to persist secret key in \"%s\" (%s) (use non-persistent key)", - SECRET_KEY_FILE, error->message); - success = FALSE; - goto out; + if (!success) + nm_log_warn (LOGD_CORE, "secret-key: failure to generate good random data for secret-key (use non-persistent key)"); + else if (nm_utils_get_testing ()) { + /* for test code, we don't write the generated secret-key to disk. */ + } else if (!nm_utils_file_set_contents (SECRET_KEY_FILE, + (const char *) new_content, + len, + 0077, + &error)) { + nm_log_warn (LOGD_CORE, "secret-key: failure to persist secret key in \"%s\" (%s) (use non-persistent key)", + SECRET_KEY_FILE, error->message); + g_clear_error (&error); + success = FALSE; + } else + nm_log_dbg (LOGD_CORE, "secret-key: persist new secret key to \"%s\"", SECRET_KEY_FILE); + + nm_explicit_bzero (rnd_buf, sizeof (rnd_buf)); + nm_explicit_bzero (new_content, sizeof (new_content)); } out: - /* regardless of success or failure, we always return a secret-key. The - * caller may choose to ignore the error and proceed. */ - *out_key_len = key_len; - *out_secret_key = secret_key; + *out_key_len = secret_len; + *out_key = nm_memdup (secret_arr, secret_len); return success; }