From 68c8ecde5729a481d7599f99914226f56b6341ff Mon Sep 17 00:00:00 2001 From: fengzero Date: Wed, 17 Jun 2026 02:53:56 +0000 Subject: [PATCH] rm neuron.key pem --- README.md | 8 ++ generate_rsa_keypair.sh | 34 ++++++ jwt_encode.py | 16 ++- plugins/restful/normal_handle.c | 10 ++ src/main.c | 1 - src/utils/neu_jwt.c | 204 +++++++++++++++++++++++--------- tests/ft/conftest.py | 40 +++++++ tests/ft/login/test_launch.py | 13 ++ 8 files changed, 266 insertions(+), 60 deletions(-) create mode 100644 generate_rsa_keypair.sh diff --git a/README.md b/README.md index 748ca281..eecaf205 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,14 @@ $ unzip neuron-dashboard.zip $ ./neuron --log ``` +On the first startup, Neuron will automatically generate `neuron.key` and `neuron.key.pub` in the config directory if they do not exist yet. + +If you want to pre-generate the key pair manually, you can still run: + +```bash +$ bash ./generate_rsa_keypair.sh ./build/config +``` + 5. Open a browser and navigate to `http://localhost:7000` to access the Neuron web interface. ## Configuration diff --git a/generate_rsa_keypair.sh b/generate_rsa_keypair.sh new file mode 100644 index 00000000..a9689ebd --- /dev/null +++ b/generate_rsa_keypair.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -euo pipefail + +output_dir="${1:-config}" +private_key_file="${output_dir}/neuron.key" +public_key_file="${output_dir}/neuron.key.pub" + +if ! command -v openssl >/dev/null 2>&1; then + echo "openssl is required but was not found in PATH." + exit 1 +fi + +mkdir -p "${output_dir}" + +if [[ -f "${private_key_file}" || -f "${public_key_file}" ]]; then + read -r -p "Existing key files found in ${output_dir}. Overwrite them? [y/N] " answer + case "${answer}" in + [yY]|[yY][eE][sS]) ;; + *) + echo "Aborted." + exit 0 + ;; + esac +fi + +umask 077 +openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${private_key_file}" +openssl pkey -in "${private_key_file}" -pubout -out "${public_key_file}" + +echo "Generated RSA key pair:" +echo " ${private_key_file}" +echo " ${public_key_file}" +echo "Run neuron with --config_dir ${output_dir} to use the generated keys." diff --git a/jwt_encode.py b/jwt_encode.py index 9c1f96e5..4189e200 100644 --- a/jwt_encode.py +++ b/jwt_encode.py @@ -1,8 +1,20 @@ import jwt # pip install pyjwt import time +from pathlib import Path -with open("neuron.key", "r") as f: - private_key = f.read() +key_candidates = [ + Path("config") / "neuron.key", + Path("neuron.key"), +] + +private_key = None +for key_path in key_candidates: + if key_path.is_file(): + private_key = key_path.read_text() + break + +if private_key is None: + raise FileNotFoundError("Cannot find neuron.key in ./config or the current directory") current_time = int(time.time()) two_years = 60 * 60 * 24 * 365 * 2 diff --git a/plugins/restful/normal_handle.c b/plugins/restful/normal_handle.c index 378d3d21..ebcb68fd 100644 --- a/plugins/restful/normal_handle.c +++ b/plugins/restful/normal_handle.c @@ -52,6 +52,16 @@ void handle_ping(nng_aio *aio) void handle_login(nng_aio *aio) { + if (disable_jwt) { + neu_json_login_resp_t login_resp = { .token = "" }; + char * result = NULL; + + neu_json_encode_by_fn(&login_resp, neu_json_encode_login_resp, &result); + neu_http_ok(aio, result); + free(result); + return; + } + NEU_PROCESS_HTTP_REQUEST( aio, neu_json_login_req_t, neu_json_decode_login_req, { neu_json_login_resp_t login_resp = { 0 }; diff --git a/src/main.c b/src/main.c index 17777f6f..fc38df23 100644 --- a/src/main.c +++ b/src/main.c @@ -39,7 +39,6 @@ static bool exit_flag = false; neu_manager_t * g_manager = NULL; zlog_category_t *neuron = NULL; -bool disable_jwt = false; bool sub_filter_err = false; int default_log_level = ZLOG_LEVEL_NOTICE; char host_port[32] = { 0 }; diff --git a/src/utils/neu_jwt.c b/src/utils/neu_jwt.c index b180916c..1fd3112f 100644 --- a/src/utils/neu_jwt.c +++ b/src/utils/neu_jwt.c @@ -20,10 +20,16 @@ #include #include #include +#include #include #include #include +#include #include +#include + +#include +#include #include @@ -39,50 +45,12 @@ struct public_key_store { int size; }; +bool disable_jwt = false; + static struct public_key_store key_store; static char neuron_private_key[2048] = { 0 }; static char neuron_public_key[2048] = { 0 }; -static const char *private_key_content = - "-----BEGIN RSA PRIVATE KEY-----\n" - "MIIEpAIBAAKCAQEAzrO7FUz4GGKl4nP5lwKMh5uageJAAHnLZpVxTR1wXA9E870s\n" - "U03gHnPWsh2bXCCx85ymYaiu/5VlptgnU0rQB1I3xGRG8eHmHPrhHNWDRKpY6oY8\n" - "AnTPqXvfa/Z4u+njJzDSqi4qM8Gwfqlgpjlg8DUF+sheLx8hL+x9XFzovTxafrmy\n" - "JRRAVQTkhB7esKKqRF8BMjdOdYZxJEv9jxa0BwXcjprPDlj5TV6k3wJqbq4UtnEE\n" - "VbjEQKqFVkfY4FuJ5NSPHpGEix94PGQGRsiWkGX0vx5udHJ0SlXM6qpMLny18FhW\n" - "bwKNX3urc3mImn/j3zcJ5x1d+FlXAXs1NBSeTQIDAQABAoIBAQDJOT8PZXbAhohn\n" - "A/AeimS0P08S0mbsD6VroGBEajxP4q2FeswD7PQZsTt4+kmcTlfuiLmQqN50AcSL\n" - "wDHIbDRIbEnN7rECGKAj5jfwEgtQdWVKKpOQ8JaYr/a466BtjyuLo4PyGC8NY6mm\n" - "JM3qBEHSlkvT7+uAhBWSye7gU7JfRPoCeFRpaMMC3Ad0DaIt/Pdt9CddNwj56+9j\n" - "gl//blNnBImjvA7/kh9gL69SYVrQoG9vm/UgFbdIwd+w2hhhpx6kuHjRJ21xIV/y\n" - "Yh9k4QhPr7zIkhQ8yn11ZhyZkMC8XAMy7NKCmAFIPNJ8jwvgBkunPEk2aYBeDUaq\n" - "a30VwlIhAoGBAOnm8DW5DROcfSj4Q8zsvgmvhGM0zzF5zmA+4E+tEbf4t6sDUjSs\n" - "ADo2EaiJZz75vLsD7UZHKzFEV/wnuT7SiNCbrJafx8Tp2UgijTkW8gEhAWREJDn5\n" - "FS375RKFdEcpmVHWgsuz4z/0L3BGxmyYWQxx5pD2NL49ZpTIHAHm/XETAoGBAOI6\n" - "8Y08TmM2bnXbL/MggYMJlqOyv27ueVYz9of3FYFAMIe7ebSgrlSCOwsoDZYmegBM\n" - "OrowR3AAnoP4fjGNbIPjNYbiv1qLOdtnvPxYotEz+ZaUBSn9V/gUEVFlgrKxgTKr\n" - "sqi/QMyyJplBTUkUuqYRDuv6TZ3OWKiDUoXTdv8fAoGBAIbbyf+PlESMY3Vtvtm2\n" - "XdODyRbR6ewiyKShW/9UT/T8iBknrwwDZ5YoeoHrxwV+RBynpPRyMCsVto0B8kKQ\n" - "bKWqPBYURb/4/Hgkw7v4yMtx1jWTPDfYryd2JptJKsOk7mtK/Nqp+wpypa9cfyc5\n" - "p44PVdqauco96Jk7zzohjlrVAoGAfjQDrJaH5DDpTjYIeckYdtFSh9+fi3LdnYk/\n" - "bnoYNRJqAE5Fhs5ccih0Z7TgX3L3fFMKL/Pe5kxyIYzuWRZcAvctVSIJPamNjShB\n" - "9UQ9EBe+lJHej54VBP+s6YuHbcg4GtxNvnVy4L5Bah1T0AEQXrQFbv8jbXU/YEJi\n" - "NuXQ7GECgYA0UNvuZxVSDn8fBxARcX3ub5aok5owdLL3SOCPMpNrmOBQj8iJGT+5\n" - "nv7fH1TWVh9yUdRwhd81s5H/PO3/myLmgq0TcPvewtEEwY+++QpsYZwHTDXgxbbw\n" - "++lmJali4fzmd/72JqocGABYhZFt0jbcQ1QEcqUYcD380+cNVZPO+w==\n" - "-----END RSA PRIVATE KEY-----\n"; - -const char *public_key_content = - "-----BEGIN PUBLIC KEY-----\n" - "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzrO7FUz4GGKl4nP5lwKM\n" - "h5uageJAAHnLZpVxTR1wXA9E870sU03gHnPWsh2bXCCx85ymYaiu/5VlptgnU0rQ\n" - "B1I3xGRG8eHmHPrhHNWDRKpY6oY8AnTPqXvfa/Z4u+njJzDSqi4qM8Gwfqlgpjlg\n" - "8DUF+sheLx8hL+x9XFzovTxafrmyJRRAVQTkhB7esKKqRF8BMjdOdYZxJEv9jxa0\n" - "BwXcjprPDlj5TV6k3wJqbq4UtnEEVbjEQKqFVkfY4FuJ5NSPHpGEix94PGQGRsiW\n" - "kGX0vx5udHJ0SlXM6qpMLny18FhWbwKNX3urc3mImn/j3zcJ5x1d+FlXAXs1NBSe\n" - "TQIDAQAB\n" - "-----END PUBLIC KEY-----\n"; - static int find_key(const char *name) { for (int i = 0; i < key_store.size; i++) { @@ -105,7 +73,8 @@ static void add_key(char *name, char *value) sizeof(key_store.key[key_store.size - 1].key) - 1); } -static char *load_key(const char *dir, char *name) +static bool load_key_file(const char *dir, const char *name, char *dst, + size_t dst_len) { FILE * f = NULL; int len = 0; @@ -117,31 +86,146 @@ static char *load_key(const char *dir, char *name) f = fopen(path, "r"); if (f == NULL) { - zlog_error(neuron, "Failed to open file: %s, errno: %d", name, errno); - return NULL; + return false; } len = fread(content, 1, sizeof(content), f); if (len <= 0) { - zlog_error(neuron, "Failed to read file: %s, errno: %d", name, errno); - fclose(f); - return NULL; + return false; } fclose(f); - return content; + + strncpy(dst, content, dst_len - 1); + dst[dst_len - 1] = '\0'; + return true; +} + +static bool write_private_key(FILE *file, EVP_PKEY *pkey) +{ + if (fchmod(fileno(file), S_IRUSR | S_IWUSR) != 0) { + return false; + } + + return PEM_write_PrivateKey(file, pkey, NULL, NULL, 0, NULL, NULL) == 1; +} + +static bool write_public_key(FILE *file, EVP_PKEY *pkey) +{ + if (fchmod(fileno(file), S_IRUSR | S_IWUSR) != 0) { + return false; + } + + return PEM_write_PUBKEY(file, pkey) == 1; +} + +static bool generate_key_pair(const char *dir_path) +{ + EVP_PKEY_CTX *ctx = NULL; + EVP_PKEY * pkey = NULL; + FILE * private_file = NULL; + FILE * public_file = NULL; + char private_path[256] = { 0 }; + char public_path[256] = { 0 }; + bool ok = false; + + snprintf(private_path, sizeof(private_path), "%s/%s", dir_path, + "neuron.key"); + snprintf(public_path, sizeof(public_path), "%s/%s", dir_path, + "neuron.key.pub"); + + ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); + if (ctx == NULL) { + return false; + } + + if (EVP_PKEY_keygen_init(ctx) <= 0 || + EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048) <= 0 || + EVP_PKEY_keygen(ctx, &pkey) <= 0) { + goto done; + } + + private_file = fopen(private_path, "w"); + if (private_file == NULL) { + goto done; + } + + public_file = fopen(public_path, "w"); + if (public_file == NULL) { + goto done; + } + + ok = write_private_key(private_file, pkey) && + write_public_key(public_file, pkey); + +done: + if (private_file != NULL) { + fclose(private_file); + } + if (public_file != NULL) { + fclose(public_file); + } + EVP_PKEY_free(pkey); + EVP_PKEY_CTX_free(ctx); + + if (!ok) { + unlink(private_path); + unlink(public_path); + } + + return ok; } static void load_neuron_key(const char *dir_path) { + char private_path[256] = { 0 }; + char public_path[256] = { 0 }; - (void) dir_path; - strncpy(neuron_private_key, private_key_content, - sizeof(neuron_private_key) - 1); + if (dir_path == NULL || *dir_path == '\0') { + zlog_error(neuron, "JWT config directory is empty"); + return; + } - strncpy(neuron_public_key, public_key_content, - sizeof(neuron_public_key) - 1); + snprintf(private_path, sizeof(private_path), "%s/%s", dir_path, + "neuron.key"); + snprintf(public_path, sizeof(public_path), "%s/%s", dir_path, + "neuron.key.pub"); + + if (!load_key_file(dir_path, "neuron.key", neuron_private_key, + sizeof(neuron_private_key)) || + !load_key_file(dir_path, "neuron.key.pub", neuron_public_key, + sizeof(neuron_public_key))) { + zlog_warn(neuron, "Missing JWT RSA key pair in %s, generating new keys", + dir_path); + if (!generate_key_pair(dir_path)) { + zlog_error(neuron, "Failed to generate JWT RSA key pair in %s", + dir_path); + neuron_private_key[0] = '\0'; + neuron_public_key[0] = '\0'; + return; + } + + zlog_notice(neuron, + "Generated JWT RSA key pair in config directory: %s", + dir_path); + + if (!load_key_file(dir_path, "neuron.key", neuron_private_key, + sizeof(neuron_private_key)) || + !load_key_file(dir_path, "neuron.key.pub", neuron_public_key, + sizeof(neuron_public_key))) { + zlog_error(neuron, + "Failed to load generated JWT RSA key pair from %s", + dir_path); + neuron_private_key[0] = '\0'; + neuron_public_key[0] = '\0'; + return; + } + } else { + zlog_notice(neuron, + "Load external RSA key pair from config directory: %s", + dir_path); + } } static void scanf_key(const char *dir_path) @@ -158,8 +242,6 @@ static void scanf_key(const char *dir_path) } while (NULL != (ptr = readdir(dir))) { - char *content = NULL; - if (!strcmp((char *) ptr->d_name, ".") || !strcmp((char *) ptr->d_name, "..")) { continue; @@ -167,8 +249,9 @@ static void scanf_key(const char *dir_path) if (strstr((char *) ptr->d_name, ".pem") != NULL || strstr((char *) ptr->d_name, ".pub") != NULL) { - content = load_key(dir_path, (char *) ptr->d_name); - assert(content != NULL); + char content[2047] = { 0 }; + assert(load_key_file(dir_path, (char *) ptr->d_name, content, + sizeof(content))); add_key((char *) ptr->d_name, content); } } @@ -178,9 +261,16 @@ static void scanf_key(const char *dir_path) int neu_jwt_init(const char *dir_path) { + if (disable_jwt) { + zlog_notice(neuron, + "HTTP auth is disabled, skip JWT RSA key generation"); + return 0; + } + load_neuron_key(dir_path); scanf_key("certs"); - return 0; + return neuron_private_key[0] == '\0' || neuron_public_key[0] == '\0' ? -1 + : 0; } int neu_jwt_new(char **token, const char *user) diff --git a/tests/ft/conftest.py b/tests/ft/conftest.py index da83e43f..b99a9e73 100644 --- a/tests/ft/conftest.py +++ b/tests/ft/conftest.py @@ -1,5 +1,11 @@ import pytest +import inspect + +import neuron.api as api +import neuron.config as config +import neuron.process as process + def pytest_addoption(parser): parser.addoption( @@ -10,3 +16,37 @@ def pytest_addoption(parser): @pytest.fixture(autouse=True, scope="session") def get_envx(request): return request.config.getoption("--env") + + +def _refresh_default_jwt(token): + old_jwt = config.default_jwt + new_jwt = f"Bearer {token}" + + config.default_jwt = new_jwt + + for _, func in inspect.getmembers(api, inspect.isfunction): + defaults = func.__defaults__ + if defaults: + func.__defaults__ = tuple( + new_jwt if value == old_jwt else value for value in defaults + ) + + kwdefaults = func.__kwdefaults__ + if kwdefaults: + func.__kwdefaults__ = { + key: new_jwt if value == old_jwt else value + for key, value in kwdefaults.items() + } + + +@pytest.fixture(autouse=True, scope="session") +def init_default_jwt(): + process.remove_persistence() + neuron = process.NeuronProcess() + neuron.start() + + response = api.login() + assert 200 == response.status_code + _refresh_default_jwt(response.json()["token"]) + + neuron.stop() diff --git a/tests/ft/login/test_launch.py b/tests/ft/login/test_launch.py index 21c28dcb..b59056e3 100644 --- a/tests/ft/login/test_launch.py +++ b/tests/ft/login/test_launch.py @@ -215,6 +215,19 @@ class TestLaunch: os.remove(dst_file) os.rename(dst_backup, dst_file) + @description(given="disable_auth enabled", when="call login", then="login should still succeed") + def test_login_with_disable_auth(self): + process.remove_persistence() + + p = process.NeuronProcess() + p.start_disable_auth() + + response = api.login() + assert 200 == response.status_code + assert "" == response.json()["token"] + + p.stop() + @description(given="encironment variables 1, command line 0", when="start neuron", then="log level set right")