v0.3.4 - Implement secure relay private key storage

- Add relay_seckey table for secure private key storage
- Implement store_relay_private_key() and get_relay_private_key() functions
- Remove relay private key from public configuration events (kind 33334)
- Update first-time startup sequence to store keys securely after DB init
- Add proper validation and error handling for private key operations
- Fix timing issue where private key storage was attempted before DB initialization
- Security improvement: relay private keys no longer exposed in public events
This commit is contained in:
Your Name
2025-09-07 07:35:51 -04:00
parent 2e8eda5c67
commit 1690b58c67
10 changed files with 148 additions and 280 deletions

View File

@@ -1,145 +0,0 @@
# CLI Port Override Implementation
## Overview
This document describes the implementation of the `-p <port>` command line option for the C Nostr Relay, which allows overriding the default relay port during first-time startup only.
## Design Principles
1. **First-time startup only**: Command line options only affect the initial configuration event creation
2. **Event-based persistence**: After first startup, all configuration is managed through database events
3. **Proper encapsulation**: All configuration logic is contained within `config.c`
4. **Extensible design**: The CLI options structure can easily accommodate future command line options
## Implementation Details
### Files Modified
#### `src/config.h`
- Added `cli_options_t` structure to encapsulate command line options
- Updated `first_time_startup_sequence()` function signature
#### `src/config.c`
- Updated `first_time_startup_sequence()` to accept CLI options parameter
- Updated `create_default_config_event()` to accept CLI options parameter
- Implemented port override logic in DEFAULT_CONFIG_VALUES array processing
#### `src/default_config_event.h`
- Updated function signature for `create_default_config_event()`
- Added proper header include for `cli_options_t` definition
#### `src/main.c`
- Added command line parsing for `-p <port>` and `--port <port>`
- Updated help text to document the new option
- Added proper error handling for invalid port values
- Updated function call to pass CLI options to configuration system
### CLI Options Structure
```c
typedef struct {
int port_override; // -1 = not set, >0 = port value
// Future CLI options can be added here
} cli_options_t;
```
### Command Line Usage
```bash
# First-time startup with port override
./c_relay_x86 -p 9090
./c_relay_x86 --port 9090
# Show help (includes new option)
./c_relay_x86 --help
# Show version
./c_relay_x86 --version
```
### Error Handling
The implementation includes robust error handling for:
- Missing port argument: `./c_relay_x86 -p`
- Invalid port format: `./c_relay_x86 -p invalid_port`
- Out-of-range ports: `./c_relay_x86 -p 0` or `./c_relay_x86 -p 99999`
## Behavior Verification
### First-Time Startup
When no database exists:
1. Command line is parsed and `-p <port>` is processed
2. CLI options are passed to `first_time_startup_sequence()`
3. Port override is applied in `create_default_config_event()`
4. Configuration event is created with overridden port value
5. Relay starts on the specified port
6. Port setting is persisted in database for future startups
### Subsequent Startups
When database already exists:
1. Command line is still parsed (for consistency)
2. Existing relay path is taken
3. Configuration is loaded from database events
4. CLI options are ignored
5. Relay starts on port from database configuration
## Testing Results
### Test 1: First-time startup with port override
```bash
./c_relay_x86 -p 9090
```
**Result**: ✅ Relay starts on port 9090, configuration stored in database
### Test 2: Subsequent startup ignores CLI options
```bash
./c_relay_x86 -p 7777
```
**Result**: ✅ Relay starts on port 9090 (from database), ignores `-p 7777`
### Test 3: Error handling
```bash
./c_relay_x86 -p invalid_port
./c_relay_x86 -p
```
**Result**: ✅ Proper error messages and help text displayed
### Test 4: Help text
```bash
./c_relay_x86 --help
```
**Result**: ✅ Displays updated help with `-p, --port PORT` option
## Database Verification
The port setting is correctly stored in the database:
```sql
SELECT json_extract(tags, '$') FROM events WHERE kind = 33334;
```
Shows the overridden port value in the configuration event tags.
## Future Extensions
The `cli_options_t` structure is designed to be easily extended:
```c
typedef struct {
int port_override; // -1 = not set, >0 = port value
char* description_override; // Future: relay description override
int max_connections_override; // Future: connection limit override
// Add more options as needed
} cli_options_t;
```
## Key Design Benefits
1. **Separation of Concerns**: Main function handles CLI parsing, config system handles application
2. **First-time Only**: Prevents confusion about configuration precedence
3. **Event-based Architecture**: Maintains consistency with the relay's event-based configuration system
4. **Extensible**: Easy to add new command line options in the future
5. **Robust**: Comprehensive error handling and validation
6. **Documented**: Clear help text explains behavior to users
## Summary
The `-p <port>` command line option implementation successfully provides a way to override the default relay port during first-time startup while maintaining the event-based configuration architecture for ongoing operation. The implementation is robust, well-tested, and ready for production use.

View File

@@ -1 +1 @@
1190476
1198669

View File

@@ -1,35 +0,0 @@
=== C Nostr Relay Server ===
Event-based configuration system
[INFO] Existing relay detected
[INFO] Initializing event-based configuration system...
[SUCCESS] Event-based configuration system initialized
[INFO] Starting existing relay...
Relay pubkey: 6df436471c7965d6473e89998162e6b87cc3547d71a2db12f559a39f4596059a
[SUCCESS] Existing relay startup prepared
[SUCCESS] Database connection established: 6df436471c7965d6473e89998162e6b87cc3547d71a2db12f559a39f4596059a.nrdb
[INFO] Database schema already exists, skipping initialization
[INFO] Existing database schema version: 4
[WARNING] No configuration event found in existing database
[SUCCESS] Relay information initialized with default values
[INFO] Initializing NIP-13 Proof of Work configuration
[INFO] PoW configured in basic validation mode (default)
[INFO] PoW Configuration: enabled=true, min_difficulty=0, validation_flags=0x1, mode=full
[INFO] Initializing NIP-40 Expiration Timestamp configuration
[INFO] Expiration Configuration: enabled=true, strict_mode=true, filter_responses=true, grace_period=300 seconds
[INFO] Subscription limits: max_per_client=25, max_total=5000
[INFO] Starting relay server...
[INFO] Starting libwebsockets-based Nostr relay server...
[INFO] Checking port availability: 8888
[WARNING] Port 8888 is in use, trying port 8889 (attempt 2/5)
[INFO] Checking port availability: 8889
[INFO] Attempting to bind libwebsockets to port 8889
[WARNING] WebSocket relay started on ws://127.0.0.1:8889 (configured port 8888 was unavailable)
[SUCCESS] WebSocket relay started on ws://127.0.0.1:8889 (configured port 8888 was unavailable)
[INFO] Received shutdown signal
[INFO] Shutting down WebSocket server...
[SUCCESS] WebSocket relay shut down cleanly
[INFO] Cleaning up configuration system...
[SUCCESS] Configuration system cleaned up
[INFO] Database connection closed
[SUCCESS] Server shutdown complete

View File

@@ -1,37 +0,0 @@
=== C Nostr Relay Server ===
Event-based configuration system
[INFO] Existing relay detected
[INFO] Initializing event-based configuration system...
[SUCCESS] Event-based configuration system initialized
[INFO] Starting existing relay...
Relay pubkey: 6df436471c7965d6473e89998162e6b87cc3547d71a2db12f559a39f4596059a
[SUCCESS] Existing relay startup prepared
[SUCCESS] Database connection established: 6df436471c7965d6473e89998162e6b87cc3547d71a2db12f559a39f4596059a.nrdb
[INFO] Database schema already exists, skipping initialization
[INFO] Existing database schema version: 4
[WARNING] No configuration event found in existing database
[SUCCESS] Relay information initialized with default values
[INFO] Initializing NIP-13 Proof of Work configuration
[INFO] PoW configured in basic validation mode (default)
[INFO] PoW Configuration: enabled=true, min_difficulty=0, validation_flags=0x1, mode=full
[INFO] Initializing NIP-40 Expiration Timestamp configuration
[INFO] Expiration Configuration: enabled=true, strict_mode=true, filter_responses=true, grace_period=300 seconds
[INFO] Subscription limits: max_per_client=25, max_total=5000
[INFO] Starting relay server...
[INFO] Starting libwebsockets-based Nostr relay server...
[INFO] Checking port availability: 8888
[WARNING] Port 8888 is in use, trying port 8889 (attempt 2/5)
[INFO] Checking port availability: 8889
[WARNING] Port 8889 is in use, trying port 8890 (attempt 3/5)
[INFO] Checking port availability: 8890
[INFO] Attempting to bind libwebsockets to port 8890
[WARNING] WebSocket relay started on ws://127.0.0.1:8890 (configured port 8888 was unavailable)
[SUCCESS] WebSocket relay started on ws://127.0.0.1:8890 (configured port 8888 was unavailable)
[INFO] Received shutdown signal
[INFO] Shutting down WebSocket server...
[SUCCESS] WebSocket relay shut down cleanly
[INFO] Cleaning up configuration system...
[SUCCESS] Configuration system cleaned up
[INFO] Database connection closed
[SUCCESS] Server shutdown complete

View File

@@ -29,6 +29,9 @@ static cJSON* g_current_config = NULL;
// Cache for initial configuration event (before database is initialized)
static cJSON* g_pending_config_event = NULL;
// Temporary storage for relay private key during first-time setup
static char g_temp_relay_privkey[65] = {0};
// ================================
// UTILITY FUNCTIONS
// ================================
@@ -437,6 +440,103 @@ int generate_random_private_key_bytes(unsigned char* privkey_bytes) {
return 0;
}
// ================================
// SECURE RELAY PRIVATE KEY STORAGE
// ================================
int store_relay_private_key(const char* relay_privkey_hex) {
if (!relay_privkey_hex) {
log_error("Invalid relay private key for storage");
return -1;
}
// Validate private key format (must be 64 hex characters)
if (strlen(relay_privkey_hex) != 64) {
log_error("Invalid relay private key length (must be 64 hex characters)");
return -1;
}
// Validate hex format
for (int i = 0; i < 64; i++) {
char c = relay_privkey_hex[i];
if (!((c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F'))) {
log_error("Invalid relay private key format (must be hex characters only)");
return -1;
}
}
if (!g_db) {
log_error("Database not available for relay private key storage");
return -1;
}
const char* sql = "INSERT OR REPLACE INTO relay_seckey (private_key_hex) VALUES (?)";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
log_error("Failed to prepare relay private key storage query");
return -1;
}
sqlite3_bind_text(stmt, 1, relay_privkey_hex, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc == SQLITE_DONE) {
log_success("Relay private key stored securely in database");
return 0;
} else {
log_error("Failed to store relay private key in database");
return -1;
}
}
char* get_relay_private_key(void) {
if (!g_db) {
log_error("Database not available for relay private key retrieval");
return NULL;
}
const char* sql = "SELECT private_key_hex FROM relay_seckey";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
log_error("Failed to prepare relay private key retrieval query");
return NULL;
}
char* private_key = NULL;
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char* key_from_db = (const char*)sqlite3_column_text(stmt, 0);
if (key_from_db && strlen(key_from_db) == 64) {
private_key = malloc(65); // 64 chars + null terminator
if (private_key) {
strcpy(private_key, key_from_db);
}
}
}
sqlite3_finalize(stmt);
if (!private_key) {
log_error("Relay private key not found in secure storage");
}
return private_key;
}
const char* get_temp_relay_private_key(void) {
if (strlen(g_temp_relay_privkey) == 64) {
return g_temp_relay_privkey;
}
return NULL;
}
// ================================
// DEFAULT CONFIG EVENT CREATION
// ================================
@@ -471,10 +571,8 @@ cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
cJSON_AddItemToArray(relay_pubkey_tag, cJSON_CreateString(relay_pubkey_hex));
cJSON_AddItemToArray(tags, relay_pubkey_tag);
cJSON* relay_privkey_tag = cJSON_CreateArray();
cJSON_AddItemToArray(relay_privkey_tag, cJSON_CreateString("relay_privkey"));
cJSON_AddItemToArray(relay_privkey_tag, cJSON_CreateString(relay_privkey_hex));
cJSON_AddItemToArray(tags, relay_privkey_tag);
// Note: relay_privkey is now stored securely in relay_seckey table
// It is no longer included in the public configuration event
// Add all default configuration values with command line overrides
for (size_t i = 0; i < DEFAULT_CONFIG_COUNT; i++) {
@@ -583,14 +681,19 @@ int first_time_startup_sequence(const cli_options_t* cli_options) {
return -1;
}
// 5. Create initial configuration event using defaults
// 5. Store relay private key in temporary storage for later secure storage
strncpy(g_temp_relay_privkey, relay_privkey, sizeof(g_temp_relay_privkey) - 1);
g_temp_relay_privkey[sizeof(g_temp_relay_privkey) - 1] = '\0';
log_info("Relay private key cached for secure storage after database initialization");
// 6. Create initial configuration event using defaults (without private key)
cJSON* config_event = create_default_config_event(admin_privkey_bytes, relay_privkey, relay_pubkey, cli_options);
if (!config_event) {
log_error("Failed to create default configuration event");
return -1;
}
// 6. Try to store configuration event in database, but cache it if database isn't ready
// 7. Try to store configuration event in database, but cache it if database isn't ready
if (store_config_event_in_database(config_event) == 0) {
log_success("Initial configuration event stored successfully");
} else {
@@ -602,16 +705,16 @@ int first_time_startup_sequence(const cli_options_t* cli_options) {
g_pending_config_event = cJSON_Duplicate(config_event, 1);
}
// 7. Cache the current config
// 8. Cache the current config
if (g_current_config) {
cJSON_Delete(g_current_config);
}
g_current_config = cJSON_Duplicate(config_event, 1);
// 8. Clean up
// 9. Clean up
cJSON_Delete(config_event);
// 9. Print admin private key for user to save
// 10. Print admin private key for user to save
printf("\n");
printf("=================================================================\n");
printf("IMPORTANT: SAVE THIS ADMIN PRIVATE KEY SECURELY!\n");

View File

@@ -79,4 +79,9 @@ int apply_runtime_config_handlers(const cJSON* old_event, const cJSON* new_event
char** find_existing_db_files(void);
char* extract_pubkey_from_filename(const char* filename);
// Secure relay private key storage functions
int store_relay_private_key(const char* relay_privkey_hex);
char* get_relay_private_key(void);
const char* get_temp_relay_private_key(void); // For first-time startup only
#endif /* CONFIG_H */

View File

@@ -3227,7 +3227,7 @@ int main(int argc, char* argv[]) {
return 1;
}
// Run first-time startup sequence (generates keys, creates database, etc.)
// Run first-time startup sequence (generates keys, sets up database path, but doesn't store private key yet)
if (first_time_startup_sequence(&cli_options) != 0) {
log_error("Failed to complete first-time startup sequence");
cleanup_configuration_system();
@@ -3243,6 +3243,23 @@ int main(int argc, char* argv[]) {
return 1;
}
// Now that database is available, store the relay private key securely
const char* relay_privkey = get_temp_relay_private_key();
if (relay_privkey) {
if (store_relay_private_key(relay_privkey) != 0) {
log_error("Failed to store relay private key securely after database initialization");
cleanup_configuration_system();
nostr_cleanup();
return 1;
}
log_success("Relay private key stored securely in database");
} else {
log_error("Relay private key not available from first-time startup");
cleanup_configuration_system();
nostr_cleanup();
return 1;
}
// Retry storing the configuration event now that database is initialized
if (retry_store_initial_config_event() != 0) {
log_warning("Failed to store initial configuration event after database init");

View File

@@ -1,12 +1,12 @@
/* Embedded SQL Schema for C Nostr Relay
* Generated from db/schema.sql - Do not edit manually
* Schema Version: 4
* Schema Version: 5
*/
#ifndef SQL_SCHEMA_H
#define SQL_SCHEMA_H
/* Schema version constant */
#define EMBEDDED_SCHEMA_VERSION "4"
#define EMBEDDED_SCHEMA_VERSION "5"
/* Embedded SQL schema as C string literal */
static const char* const EMBEDDED_SCHEMA_SQL =
@@ -15,7 +15,7 @@ static const char* const EMBEDDED_SCHEMA_SQL =
-- Event-based configuration system using kind 33334 Nostr events\n\
\n\
-- Schema version tracking\n\
PRAGMA user_version = 4;\n\
PRAGMA user_version = 5;\n\
\n\
-- Enable foreign key support\n\
PRAGMA foreign_keys = ON;\n\
@@ -58,8 +58,8 @@ CREATE TABLE schema_info (\n\
\n\
-- Insert schema metadata\n\
INSERT INTO schema_info (key, value) VALUES\n\
('version', '4'),\n\
('description', 'Event-based Nostr relay schema with kind 33334 configuration events'),\n\
('version', '5'),\n\
('description', 'Event-based Nostr relay schema with secure relay private key storage'),\n\
('created_at', strftime('%s', 'now'));\n\
\n\
-- Helper views for common queries\n\
@@ -128,6 +128,13 @@ BEGIN\n\
AND id != NEW.id;\n\
END;\n\
\n\
-- Relay Private Key Secure Storage\n\
-- Stores the relay's private key separately from public configuration\n\
CREATE TABLE relay_seckey (\n\
private_key_hex TEXT NOT NULL CHECK (length(private_key_hex) = 64),\n\
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\
);\n\
\n\
-- Persistent Subscriptions Logging Tables (Phase 2)\n\
-- Optional database logging for subscription analytics and debugging\n\
\n\

View File

@@ -1,47 +0,0 @@
=== C Nostr Relay Server ===
Event-based configuration system
[INFO] Existing relay detected
[INFO] Initializing event-based configuration system...
[SUCCESS] Event-based configuration system initialized
[INFO] Starting existing relay...
Relay pubkey: 6df436471c7965d6473e89998162e6b87cc3547d71a2db12f559a39f4596059a
[SUCCESS] Existing relay startup prepared
[SUCCESS] Database connection established: 6df436471c7965d6473e89998162e6b87cc3547d71a2db12f559a39f4596059a.nrdb
[INFO] Database schema already exists, skipping initialization
[INFO] Existing database schema version: 4
[INFO] Applying configuration from event...
[INFO] Checking for runtime configuration changes...
[INFO] Subscription limits changed - updating subscription manager
[INFO] Subscription limits: max_per_client=25, max_total=5000
[INFO] PoW configuration changed - reinitializing PoW system
[INFO] Initializing NIP-13 Proof of Work configuration
[INFO] PoW configured in basic validation mode
[INFO] PoW Configuration: enabled=true, min_difficulty=0, validation_flags=0x1, mode=full
[INFO] Expiration configuration changed - reinitializing expiration system
[INFO] Initializing NIP-40 Expiration Timestamp configuration
[INFO] Expiration Configuration: enabled=true, strict_mode=true, filter_responses=true, grace_period=300 seconds
[INFO] Relay information changed - reinitializing relay info
[SUCCESS] Relay information initialized with default values
[SUCCESS] Configuration updated via kind 33334 event - 4 system components reinitialized
[SUCCESS] Configuration applied from event (4 handlers executed)
[SUCCESS] Configuration loaded from database
[SUCCESS] Relay information initialized with default values
[INFO] Initializing NIP-13 Proof of Work configuration
[INFO] PoW configured in basic validation mode
[INFO] PoW Configuration: enabled=true, min_difficulty=0, validation_flags=0x1, mode=full
[INFO] Initializing NIP-40 Expiration Timestamp configuration
[INFO] Expiration Configuration: enabled=true, strict_mode=true, filter_responses=true, grace_period=300 seconds
[INFO] Subscription limits: max_per_client=25, max_total=5000
[INFO] Starting relay server...
[INFO] Starting libwebsockets-based Nostr relay server...
[INFO] Attempting to bind to port 8888
[2025/09/06 20:34:16:8170] E: ERROR on binding fd 8 to port 8888 (-1 98)
[2025/09/06 20:34:16:8172] E: init server failed
[2025/09/06 20:34:16:8172] E: Failed to create default vhost
[ERROR] Failed to create libwebsockets context after 1 attempts. Last attempted port: 8888
libwebsockets creation error: Inappropriate ioctl for device
[INFO] Cleaning up configuration system...
[SUCCESS] Configuration system cleaned up
[INFO] Database connection closed
[ERROR] Server shutdown with errors