/* * C-Relay Request Validator - Integrated Authentication System * * Provides complete request validation including: * - Protocol validation via nostr_core_lib (signatures, pubkey extraction, * NIP-42) * - Database-driven authorization rules (whitelist, blacklist, size limits) * - Memory caching for performance * - SQLite integration for C-relay needs */ #define _GNU_SOURCE #include "../nostr_core_lib/cjson/cJSON.h" #include "../nostr_core_lib/nostr_core/nip001.h" #include "../nostr_core_lib/nostr_core/nip013.h" // NIP-13: Proof of Work #include "../nostr_core_lib/nostr_core/nostr_common.h" #include "../nostr_core_lib/nostr_core/utils.h" #include "config.h" // C-relay configuration system #include #include #include #include #include #include // External references to C-relay global state extern sqlite3* g_db; extern char g_database_path[512]; // Forward declaration for C-relay event storage function extern int store_event(cJSON* event); // External references to C-relay global PoW configuration (matching main.c) extern struct pow_config { int enabled; // 0 = disabled, 1 = enabled int min_pow_difficulty; // Minimum required difficulty (0 = no requirement) int validation_flags; // Bitflags for validation options int require_nonce_tag; // 1 = require nonce tag presence int reject_lower_targets; // 1 = reject if committed < actual difficulty int strict_format; // 1 = enforce strict nonce tag format int anti_spam_mode; // 1 = full anti-spam validation } g_pow_config; // External references to C-relay expiration configuration (matching main.c) extern struct expiration_config { int enabled; // 0 = disabled, 1 = enabled int strict_mode; // 1 = reject expired events on submission int filter_responses; // 1 = filter expired events from responses int delete_expired; // 1 = delete expired events from DB (future feature) long grace_period; // Grace period in seconds for clock skew } g_expiration_config; // Configuration functions from C-relay extern int get_config_bool(const char* key, int default_value); extern int get_config_int(const char* key, int default_value); // NIP-42 constants (from nostr_core_lib) #define NOSTR_NIP42_AUTH_EVENT_KIND 22242 // NIP-42 error codes (from nostr_core_lib) #define NOSTR_ERROR_NIP42_CHALLENGE_NOT_FOUND -200 #define NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED -201 #define NOSTR_ERROR_NIP42_INVALID_CHALLENGE -202 #define NOSTR_ERROR_NIP42_URL_MISMATCH -203 #define NOSTR_ERROR_NIP42_TIME_TOLERANCE -204 #define NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID -205 #define NOSTR_ERROR_NIP42_INVALID_RELAY_URL -206 #define NOSTR_ERROR_NIP42_NOT_CONFIGURED -207 // Forward declarations for NIP-42 functions (simple implementations for C-relay) int nostr_nip42_generate_challenge(char *challenge_buffer, size_t buffer_size); int nostr_nip42_verify_auth_event(cJSON *event, const char *challenge_id, const char *relay_url, int time_tolerance_seconds); // Type definitions for unified request validation typedef struct { const char *operation; // HTTP method or operation type const char *auth_header; // Authorization header value const char *client_ip; // Client IP address const char *request_url; // Full request URL const char *resource_hash; // Resource hash for validation const char *challenge_id; // NIP-42 challenge ID long file_size; // File size for upload validation int nip42_enabled; // Whether NIP-42 is enabled } nostr_unified_request_t; typedef struct { int valid; // Whether request is valid int error_code; // Error code if invalid char reason[512]; // Reason for validation result char pubkey[65]; // Extracted pubkey from event unsigned char *file_data; // File data for uploads size_t file_size; // Size of file data int owns_file_data; // Whether we own the file data memory char expected_hash[65]; // Expected file hash } nostr_request_result_t; // Additional error codes for ginxsom-specific functionality #define NOSTR_ERROR_CRYPTO_INIT -100 #define NOSTR_ERROR_AUTH_REQUIRED -101 #define NOSTR_ERROR_NIP42_DISABLED -102 #define NOSTR_ERROR_EVENT_EXPIRED -103 // Note: NOSTR_ERROR_NIP42_CHALLENGE_NOT_FOUND and // NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED are already defined in // nostr_core_lib/nostr_core/nostr_common.h // Note: Using g_database_path from C-relay global state instead of hardcoded path // NIP-42 challenge management constants #define MAX_CHALLENGES 1000 #define CHALLENGE_CLEANUP_INTERVAL 300 // 5 minutes //============================================================================= // DATA STRUCTURES //============================================================================= // NIP-42 challenge storage typedef struct { char challenge_id[65]; char client_ip[64]; time_t created_at; time_t expires_at; int active; } nip42_challenge_entry_t; // NIP-42 challenge management typedef struct { nip42_challenge_entry_t challenges[MAX_CHALLENGES]; int challenge_count; time_t last_cleanup; int timeout_seconds; int time_tolerance_seconds; } nip42_challenge_manager_t; // Cached configuration structure typedef struct { int auth_required; // Whether authentication is required long max_file_size; // Maximum file size in bytes int admin_enabled; // Whether admin interface is enabled char admin_pubkey[65]; // Admin public key int nip42_mode; // NIP-42 authentication mode int nip42_challenge_timeout; // NIP-42 challenge timeout in seconds int nip42_time_tolerance; // NIP-42 time tolerance in seconds time_t cache_expires; // When cache expires int cache_valid; // Whether cache is valid } auth_config_cache_t; //============================================================================= // GLOBAL STATE //============================================================================= static auth_config_cache_t g_auth_cache = {0}; static nip42_challenge_manager_t g_challenge_manager = {0}; static int g_validator_initialized = 0; // Last rule violation details for status code mapping struct { char violation_type[100]; // "pubkey_blacklist", "hash_blacklist", // "whitelist_violation", etc. char reason[500]; // specific reason string } g_last_rule_violation = {0}; /** * Helper function for consistent debug logging to main relay.log file */ static void validator_debug_log(const char *message) { FILE *relay_log = fopen("relay.log", "a"); if (relay_log) { // Use same format as main logging system time_t now = time(NULL); struct tm *tm_info = localtime(&now); char timestamp[20]; strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info); fprintf(relay_log, "[%s] [DEBUG] %s", timestamp, message); fclose(relay_log); } } //============================================================================= // FORWARD DECLARATIONS //============================================================================= static int reload_auth_config(void); // Removed unused forward declarations for functions that are no longer called static int check_database_auth_rules(const char *pubkey, const char *operation, const char *resource_hash); void nostr_request_validator_clear_violation(void); // NIP-42 challenge management functions static void cleanup_expired_challenges(void); static int store_challenge(const char *challenge_id, const char *client_ip); static int generate_challenge_id(char *challenge_buffer, size_t buffer_size); //============================================================================= // MAIN API FUNCTIONS //============================================================================= /** * Initialize the ginxsom request validator system */ int ginxsom_request_validator_init(const char *db_path, const char *app_name) { // Mark db_path as unused to suppress warning - it's for future use (void)db_path; (void)app_name; if (g_validator_initialized) { return NOSTR_SUCCESS; // Already initialized } // Initialize nostr_core_lib if not already done if (nostr_crypto_init() != NOSTR_SUCCESS) { validator_debug_log( "VALIDATOR: Failed to initialize nostr crypto system\n"); return NOSTR_ERROR_CRYPTO_INIT; } // Load initial configuration from database int result = reload_auth_config(); if (result != NOSTR_SUCCESS) { validator_debug_log( "VALIDATOR: Failed to load configuration from database\n"); return result; } // Initialize NIP-42 challenge manager memset(&g_challenge_manager, 0, sizeof(g_challenge_manager)); g_challenge_manager.timeout_seconds = g_auth_cache.nip42_challenge_timeout > 0 ? g_auth_cache.nip42_challenge_timeout : 600; g_challenge_manager.time_tolerance_seconds = g_auth_cache.nip42_time_tolerance > 0 ? g_auth_cache.nip42_time_tolerance : 300; g_challenge_manager.last_cleanup = time(NULL); g_validator_initialized = 1; validator_debug_log( "VALIDATOR: Request validator initialized successfully\n"); return NOSTR_SUCCESS; } /** * Check if authentication rules are enabled */ int nostr_auth_rules_enabled(void) { // Reload config if cache expired if (!g_auth_cache.cache_valid || time(NULL) > g_auth_cache.cache_expires) { reload_auth_config(); } return g_auth_cache.auth_required; } /////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////// // MAIN VALIDATION OF REQUEST /////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////// int nostr_validate_unified_request(const char* json_string, size_t json_length) { // Clear previous violation details nostr_request_validator_clear_violation(); ///////////////////////////////////////////////////////////////////// // PHASE 1: INPUT VALIDATION (Immediate Rejection ~1μs) ///////////////////////////////////////////////////////////////////// // 1. Null Pointer Checks - Reject malformed requests instantly if (!json_string || json_length == 0) { validator_debug_log("VALIDATOR_DEBUG: STEP 1 FAILED - Null input\n"); return NOSTR_ERROR_INVALID_INPUT; } // 2. Initialization Check - Verify system is properly initialized if (!g_validator_initialized) { validator_debug_log("VALIDATOR_DEBUG: STEP 2 FAILED - Validator not initialized\n"); return NOSTR_ERROR_INVALID_INPUT; } // 3. Parse JSON string to cJSON event object cJSON *event = cJSON_ParseWithLength(json_string, json_length); if (!event) { validator_debug_log("VALIDATOR_DEBUG: STEP 3 FAILED - Failed to parse JSON event\n"); return NOSTR_ERROR_INVALID_INPUT; } // 4. Validate basic event structure cJSON *id = cJSON_GetObjectItem(event, "id"); cJSON *pubkey = cJSON_GetObjectItem(event, "pubkey"); cJSON *created_at = cJSON_GetObjectItem(event, "created_at"); cJSON *kind = cJSON_GetObjectItem(event, "kind"); cJSON *tags = cJSON_GetObjectItem(event, "tags"); cJSON *content = cJSON_GetObjectItem(event, "content"); cJSON *sig = cJSON_GetObjectItem(event, "sig"); if (!id || !cJSON_IsString(id) || !pubkey || !cJSON_IsString(pubkey) || !created_at || !cJSON_IsNumber(created_at) || !kind || !cJSON_IsNumber(kind) || !tags || !cJSON_IsArray(tags) || !content || !cJSON_IsString(content) || !sig || !cJSON_IsString(sig)) { validator_debug_log("VALIDATOR_DEBUG: STEP 4 FAILED - Invalid event structure\n"); cJSON_Delete(event); return NOSTR_ERROR_INVALID_INPUT; } int event_kind = (int)cJSON_GetNumberValue(kind); // 5. Reload config if needed if (!g_auth_cache.cache_valid || time(NULL) > g_auth_cache.cache_expires) { reload_auth_config(); } char config_msg[256]; sprintf(config_msg, "VALIDATOR_DEBUG: STEP 5 PASSED - Event kind: %d, auth_required: %d\n", event_kind, g_auth_cache.auth_required); validator_debug_log(config_msg); ///////////////////////////////////////////////////////////////////// // PHASE 2: NOSTR EVENT VALIDATION ///////////////////////////////////////////////////////////////////// // 6. Nostr Event Structure Validation using nostr_core_lib int validation_result = nostr_validate_event(event); if (validation_result != NOSTR_SUCCESS) { char validation_msg[256]; sprintf(validation_msg, "VALIDATOR_DEBUG: STEP 6 FAILED - NOSTR event validation failed (error=%d)\n", validation_result); validator_debug_log(validation_msg); cJSON_Delete(event); return validation_result; } validator_debug_log("VALIDATOR_DEBUG: STEP 6 PASSED - Event structure and signature valid\n"); // 7. Extract pubkey for rule evaluation const char *event_pubkey = cJSON_GetStringValue(pubkey); if (!event_pubkey || strlen(event_pubkey) != 64) { validator_debug_log("VALIDATOR_DEBUG: STEP 7 FAILED - Invalid pubkey format\n"); cJSON_Delete(event); return NOSTR_ERROR_EVENT_INVALID_PUBKEY; } char pubkey_msg[256]; sprintf(pubkey_msg, "VALIDATOR_DEBUG: STEP 7 PASSED - Extracted pubkey: %.16s...\n", event_pubkey); validator_debug_log(pubkey_msg); ///////////////////////////////////////////////////////////////////// // PHASE 3: EVENT KIND SPECIFIC VALIDATION ///////////////////////////////////////////////////////////////////// // 8. Handle NIP-42 authentication challenge events (kind 22242) if (event_kind == 22242) { validator_debug_log("VALIDATOR_DEBUG: STEP 8 - Processing NIP-42 challenge response\n"); if (g_auth_cache.nip42_mode == 0) { validator_debug_log("VALIDATOR_DEBUG: STEP 8 FAILED - NIP-42 is disabled\n"); cJSON_Delete(event); return NOSTR_ERROR_NIP42_DISABLED; } // TODO: Implement full NIP-42 challenge validation // For now, accept all valid NIP-42 events validator_debug_log("VALIDATOR_DEBUG: STEP 8 PASSED - NIP-42 challenge response accepted\n"); cJSON_Delete(event); return NOSTR_SUCCESS; } ///////////////////////////////////////////////////////////////////// // PHASE 4: AUTHENTICATION RULES (Database Queries) ///////////////////////////////////////////////////////////////////// // 9. Check if authentication rules are enabled if (!g_auth_cache.auth_required) { validator_debug_log("VALIDATOR_DEBUG: STEP 9 - Authentication disabled, skipping database auth rules\n"); } else { // 10. Check database authentication rules (only if auth enabled) validator_debug_log("VALIDATOR_DEBUG: STEP 10 - Checking database authentication rules\n"); // Create operation string with event kind for more specific rule matching char operation_str[64]; snprintf(operation_str, sizeof(operation_str), "event_kind_%d", event_kind); // Also check generic "event" operation for backward compatibility int rules_result = check_database_auth_rules(event_pubkey, "event", NULL); if (rules_result != NOSTR_SUCCESS) { // If generic event check fails, try specific event kind check rules_result = check_database_auth_rules(event_pubkey, operation_str, NULL); if (rules_result != NOSTR_SUCCESS) { char rules_msg[256]; sprintf(rules_msg, "VALIDATOR_DEBUG: STEP 10 FAILED - Database rules denied request (kind=%d)\n", event_kind); validator_debug_log(rules_msg); cJSON_Delete(event); return rules_result; } } char rules_success_msg[256]; sprintf(rules_success_msg, "VALIDATOR_DEBUG: STEP 10 PASSED - Database rules allow request (kind=%d)\n", event_kind); validator_debug_log(rules_success_msg); } ///////////////////////////////////////////////////////////////////// // PHASE 5: ADDITIONAL VALIDATIONS (C-relay specific) ///////////////////////////////////////////////////////////////////// // 11. NIP-13 Proof of Work validation if (g_pow_config.enabled && g_pow_config.min_pow_difficulty > 0) { validator_debug_log("VALIDATOR_DEBUG: STEP 11 - Validating NIP-13 Proof of Work\n"); nostr_pow_result_t pow_result; int pow_validation_result = nostr_validate_pow(event, g_pow_config.min_pow_difficulty, g_pow_config.validation_flags, &pow_result); if (pow_validation_result != NOSTR_SUCCESS) { char pow_msg[256]; sprintf(pow_msg, "VALIDATOR_DEBUG: STEP 11 FAILED - PoW validation failed (error=%d, difficulty=%d/%d)\n", pow_validation_result, pow_result.actual_difficulty, g_pow_config.min_pow_difficulty); validator_debug_log(pow_msg); cJSON_Delete(event); return pow_validation_result; } char pow_success_msg[256]; sprintf(pow_success_msg, "VALIDATOR_DEBUG: STEP 11 PASSED - PoW validated (difficulty=%d, target=%d)\n", pow_result.actual_difficulty, pow_result.committed_target); validator_debug_log(pow_success_msg); } else { validator_debug_log("VALIDATOR_DEBUG: STEP 11 SKIPPED - PoW validation disabled or min_difficulty=0\n"); } // 12. NIP-40 Expiration validation // Always check expiration tags if present (following NIP-40 specification) validator_debug_log("VALIDATOR_DEBUG: STEP 12 - Starting NIP-40 Expiration validation\n"); cJSON *expiration_tag = NULL; cJSON *tags_array = cJSON_GetObjectItem(event, "tags"); if (tags_array && cJSON_IsArray(tags_array)) { cJSON *tag = NULL; cJSON_ArrayForEach(tag, tags_array) { if (!cJSON_IsArray(tag)) continue; cJSON *tag_name = cJSON_GetArrayItem(tag, 0); if (!tag_name || !cJSON_IsString(tag_name)) continue; const char *tag_name_str = cJSON_GetStringValue(tag_name); if (strcmp(tag_name_str, "expiration") == 0) { cJSON *tag_value = cJSON_GetArrayItem(tag, 1); if (tag_value && cJSON_IsString(tag_value)) { expiration_tag = tag_value; break; } } } } if (expiration_tag) { const char *expiration_str = cJSON_GetStringValue(expiration_tag); // Validate that the expiration string contains only digits (and optional leading whitespace) const char* p = expiration_str; // Skip leading whitespace while (*p == ' ' || *p == '\t') p++; // Check if we have at least one digit if (*p == '\0') { validator_debug_log("VALIDATOR_DEBUG: STEP 12 SKIPPED - Empty expiration tag value, ignoring\n"); } else { // Validate that all remaining characters are digits const char* digit_start = p; while (*p >= '0' && *p <= '9') p++; // If we didn't consume the entire string or found no digits, it's malformed if (*p != '\0' || p == digit_start) { char malformed_msg[256]; sprintf(malformed_msg, "VALIDATOR_DEBUG: STEP 12 SKIPPED - Malformed expiration tag value '%.32s', ignoring\n", expiration_str); validator_debug_log(malformed_msg); } else { // Valid numeric string, parse and check expiration time_t expiration_time = (time_t)atol(expiration_str); time_t now = time(NULL); int grace_period = get_config_int("nip40_expiration_grace_period", 60); if (expiration_time > 0 && now > expiration_time + grace_period) { char exp_msg[256]; sprintf(exp_msg, "VALIDATOR_DEBUG: STEP 12 FAILED - Event expired (now=%ld, exp=%ld, grace=%d)\n", (long)now, (long)expiration_time, grace_period); validator_debug_log(exp_msg); cJSON_Delete(event); return NOSTR_ERROR_EVENT_EXPIRED; } char exp_success_msg[256]; sprintf(exp_success_msg, "VALIDATOR_DEBUG: STEP 12 PASSED - Event not expired (exp=%ld, now=%ld)\n", (long)expiration_time, (long)now); validator_debug_log(exp_success_msg); } } } else { validator_debug_log("VALIDATOR_DEBUG: STEP 12 SKIPPED - No expiration tag found\n"); } // All validations passed validator_debug_log("VALIDATOR_DEBUG: STEP 13 PASSED - All validations complete, event ACCEPTED\n"); cJSON_Delete(event); return NOSTR_SUCCESS; } /** * Generate NIP-42 challenge for clients */ int nostr_request_validator_generate_nip42_challenge(void *challenge_struct, const char *client_ip) { // Mark client_ip as unused to suppress warning - it's for future enhancement (void)client_ip; // Use nostr_core_lib NIP-42 functionality char challenge_id[65]; int result = nostr_nip42_generate_challenge(challenge_id, 32); if (result != NOSTR_SUCCESS) { return result; } // Fill challenge structure (assuming it's a compatible structure) // This is a simplified implementation - adjust based on actual structure // needs if (challenge_struct) { // Cast to appropriate structure and fill fields // For now, just return success } return NOSTR_SUCCESS; } /** * Get the last rule violation type for status code mapping */ const char *nostr_request_validator_get_last_violation_type(void) { return g_last_rule_violation.violation_type; } /** * Clear the last rule violation details */ void nostr_request_validator_clear_violation(void) { memset(&g_last_rule_violation, 0, sizeof(g_last_rule_violation)); } /** * Cleanup request validator resources */ void ginxsom_request_validator_cleanup(void) { g_validator_initialized = 0; memset(&g_auth_cache, 0, sizeof(g_auth_cache)); nostr_request_validator_clear_violation(); } /** * Free file data allocated by validator */ void nostr_request_result_free_file_data(nostr_request_result_t *result) { if (result && result->file_data && result->owns_file_data) { free(result->file_data); result->file_data = NULL; result->file_size = 0; result->owns_file_data = 0; } } //============================================================================= // HELPER FUNCTIONS //============================================================================= /** * Get cache timeout from environment variable or default */ static int get_cache_timeout(void) { char *no_cache = getenv("GINX_NO_CACHE"); char *cache_timeout = getenv("GINX_CACHE_TIMEOUT"); if (no_cache && strcmp(no_cache, "1") == 0) { return 0; // No caching } if (cache_timeout) { int timeout = atoi(cache_timeout); return (timeout >= 0) ? timeout : 300; // Use provided value or default } return 300; // Default 5 minutes } /** * Force cache refresh - invalidates current cache */ void nostr_request_validator_force_cache_refresh(void) { g_auth_cache.cache_valid = 0; g_auth_cache.cache_expires = 0; validator_debug_log("VALIDATOR: Cache forcibly invalidated\n"); } /** * Reload authentication configuration from unified config table */ static int reload_auth_config(void) { sqlite3 *db = NULL; sqlite3_stmt *stmt = NULL; int rc; // Clear cache memset(&g_auth_cache, 0, sizeof(g_auth_cache)); // Open database using global database path if (strlen(g_database_path) == 0) { validator_debug_log("VALIDATOR: No database path available\n"); // Use defaults g_auth_cache.auth_required = 0; g_auth_cache.max_file_size = 104857600; // 100MB g_auth_cache.admin_enabled = 0; g_auth_cache.nip42_mode = 1; // Optional int cache_timeout = get_cache_timeout(); g_auth_cache.cache_expires = time(NULL) + cache_timeout; g_auth_cache.cache_valid = 1; return NOSTR_SUCCESS; } rc = sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc != SQLITE_OK) { validator_debug_log("VALIDATOR: Could not open database\n"); // Use defaults g_auth_cache.auth_required = 0; g_auth_cache.max_file_size = 104857600; // 100MB g_auth_cache.admin_enabled = 0; g_auth_cache.nip42_mode = 1; // Optional int cache_timeout = get_cache_timeout(); g_auth_cache.cache_expires = time(NULL) + cache_timeout; g_auth_cache.cache_valid = 1; return NOSTR_SUCCESS; } // Load configuration values from unified config table const char *config_sql = "SELECT key, value FROM config WHERE key IN ('require_auth', " "'auth_rules_enabled', 'max_file_size', 'admin_enabled', 'admin_pubkey', " "'nip42_require_auth', 'nip42_challenge_timeout', " "'nip42_time_tolerance')"; rc = sqlite3_prepare_v2(db, config_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { const char *key = (const char *)sqlite3_column_text(stmt, 0); const char *value = (const char *)sqlite3_column_text(stmt, 1); if (!key || !value) continue; if (strcmp(key, "require_auth") == 0) { g_auth_cache.auth_required = (strcmp(value, "true") == 0) ? 1 : 0; } else if (strcmp(key, "auth_rules_enabled") == 0) { // Override auth_required with auth_rules_enabled if present (higher // priority) g_auth_cache.auth_required = (strcmp(value, "true") == 0) ? 1 : 0; } else if (strcmp(key, "max_file_size") == 0) { g_auth_cache.max_file_size = atol(value); } else if (strcmp(key, "admin_enabled") == 0) { g_auth_cache.admin_enabled = (strcmp(value, "true") == 0) ? 1 : 0; } else if (strcmp(key, "admin_pubkey") == 0) { strncpy(g_auth_cache.admin_pubkey, value, sizeof(g_auth_cache.admin_pubkey) - 1); } else if (strcmp(key, "nip42_require_auth") == 0) { if (strcmp(value, "false") == 0) { g_auth_cache.nip42_mode = 0; // Disabled } else if (strcmp(value, "required") == 0) { g_auth_cache.nip42_mode = 2; // Required } else if (strcmp(value, "true") == 0) { g_auth_cache.nip42_mode = 1; // Optional/Enabled } else { g_auth_cache.nip42_mode = 1; // Default to Optional/Enabled } } else if (strcmp(key, "nip42_challenge_timeout") == 0) { g_auth_cache.nip42_challenge_timeout = atoi(value); } else if (strcmp(key, "nip42_time_tolerance") == 0) { g_auth_cache.nip42_time_tolerance = atoi(value); } } sqlite3_finalize(stmt); } sqlite3_close(db); // Set cache expiration with environment variable support int cache_timeout = get_cache_timeout(); g_auth_cache.cache_expires = time(NULL) + cache_timeout; g_auth_cache.cache_valid = 1; // Set defaults for missing values if (g_auth_cache.max_file_size == 0) { g_auth_cache.max_file_size = 104857600; // 100MB } // Debug logging fprintf(stderr, "VALIDATOR: Configuration loaded from unified config table - " "auth_required: %d, max_file_size: %ld, nip42_mode: %d, " "cache_timeout: %d\n", g_auth_cache.auth_required, g_auth_cache.max_file_size, g_auth_cache.nip42_mode, cache_timeout); fprintf(stderr, "VALIDATOR: NIP-42 mode details - nip42_mode=%d (0=disabled, " "1=optional/enabled, 2=required)\n", g_auth_cache.nip42_mode); return NOSTR_SUCCESS; } // Removed unused functions: parse_authorization_header, extract_pubkey_from_event // Note: Blossom protocol validation removed - C-relay uses standard Nostr events only /** * Check database authentication rules for the request * Implements the 6-step rule evaluation engine from AUTH_API.md */ static int check_database_auth_rules(const char *pubkey, const char *operation, const char *resource_hash) { sqlite3 *db = NULL; sqlite3_stmt *stmt = NULL; int rc; if (!pubkey) { validator_debug_log( "VALIDATOR_DEBUG: RULES ENGINE - Missing pubkey for rule evaluation\n"); return NOSTR_ERROR_INVALID_INPUT; } char rules_msg[256]; sprintf(rules_msg, "VALIDATOR_DEBUG: RULES ENGINE - Checking rules for pubkey=%.32s..., " "operation=%s\n", pubkey, operation ? operation : "NULL"); validator_debug_log(rules_msg); // Open database using global database path if (strlen(g_database_path) == 0) { validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - No database path available\n"); return NOSTR_SUCCESS; // Default allow on DB error } rc = sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc != SQLITE_OK) { validator_debug_log( "VALIDATOR_DEBUG: RULES ENGINE - Failed to open database\n"); return NOSTR_SUCCESS; // Default allow on DB error } // Step 1: Check pubkey blacklist (highest priority) const char *blacklist_sql = "SELECT rule_type, description FROM auth_rules WHERE rule_type = " "'pubkey_blacklist' AND rule_target = ? AND operation = ? AND enabled = " "1 ORDER BY priority LIMIT 1"; rc = sqlite3_prepare_v2(db, blacklist_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC); if (sqlite3_step(stmt) == SQLITE_ROW) { const char *description = (const char *)sqlite3_column_text(stmt, 1); validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 1 FAILED - " "Pubkey blacklisted\n"); char blacklist_msg[256]; sprintf(blacklist_msg, "VALIDATOR_DEBUG: RULES ENGINE - Blacklist rule matched: %s\n", description ? description : "Unknown"); validator_debug_log(blacklist_msg); // Set specific violation details for status code mapping strcpy(g_last_rule_violation.violation_type, "pubkey_blacklist"); sprintf(g_last_rule_violation.reason, "%s: Public key blacklisted", description ? description : "TEST_PUBKEY_BLACKLIST"); sqlite3_finalize(stmt); sqlite3_close(db); return NOSTR_ERROR_AUTH_REQUIRED; } sqlite3_finalize(stmt); } validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 1 PASSED - Pubkey " "not blacklisted\n"); // Step 2: Check hash blacklist if (resource_hash) { const char *hash_blacklist_sql = "SELECT rule_type, description FROM auth_rules WHERE rule_type = " "'hash_blacklist' AND rule_target = ? AND operation = ? AND enabled = " "1 ORDER BY priority LIMIT 1"; rc = sqlite3_prepare_v2(db, hash_blacklist_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, resource_hash, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC); if (sqlite3_step(stmt) == SQLITE_ROW) { const char *description = (const char *)sqlite3_column_text(stmt, 1); validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 2 FAILED - " "Hash blacklisted\n"); char hash_blacklist_msg[256]; sprintf( hash_blacklist_msg, "VALIDATOR_DEBUG: RULES ENGINE - Hash blacklist rule matched: %s\n", description ? description : "Unknown"); validator_debug_log(hash_blacklist_msg); // Set specific violation details for status code mapping strcpy(g_last_rule_violation.violation_type, "hash_blacklist"); sprintf(g_last_rule_violation.reason, "%s: File hash blacklisted", description ? description : "TEST_HASH_BLACKLIST"); sqlite3_finalize(stmt); sqlite3_close(db); return NOSTR_ERROR_AUTH_REQUIRED; } sqlite3_finalize(stmt); } validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 2 PASSED - Hash " "not blacklisted\n"); } else { validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 2 SKIPPED - No " "resource hash provided\n"); } // Step 3: Check pubkey whitelist const char *whitelist_sql = "SELECT rule_type, description FROM auth_rules WHERE rule_type = " "'pubkey_whitelist' AND rule_target = ? AND operation = ? AND enabled = " "1 ORDER BY priority LIMIT 1"; rc = sqlite3_prepare_v2(db, whitelist_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC); if (sqlite3_step(stmt) == SQLITE_ROW) { const char *description = (const char *)sqlite3_column_text(stmt, 1); validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 PASSED - " "Pubkey whitelisted\n"); char whitelist_msg[256]; sprintf(whitelist_msg, "VALIDATOR_DEBUG: RULES ENGINE - Whitelist rule matched: %s\n", description ? description : "Unknown"); validator_debug_log(whitelist_msg); sqlite3_finalize(stmt); sqlite3_close(db); return NOSTR_SUCCESS; // Allow whitelisted pubkey } sqlite3_finalize(stmt); } validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 FAILED - Pubkey " "not whitelisted\n"); // Step 4: Check if any whitelist rules exist - if yes, deny by default const char *whitelist_exists_sql = "SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'pubkey_whitelist' " "AND operation = ? AND enabled = 1 LIMIT 1"; rc = sqlite3_prepare_v2(db, whitelist_exists_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, operation ? operation : "", -1, SQLITE_STATIC); if (sqlite3_step(stmt) == SQLITE_ROW) { int whitelist_count = sqlite3_column_int(stmt, 0); if (whitelist_count > 0) { validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 4 FAILED - " "Whitelist exists but pubkey not in it\n"); // Set specific violation details for status code mapping strcpy(g_last_rule_violation.violation_type, "whitelist_violation"); strcpy(g_last_rule_violation.reason, "Public key not whitelisted for this operation"); sqlite3_finalize(stmt); sqlite3_close(db); return NOSTR_ERROR_AUTH_REQUIRED; } } sqlite3_finalize(stmt); } validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 4 PASSED - No " "whitelist restrictions apply\n"); sqlite3_close(db); validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 PASSED - All " "rule checks completed, default ALLOW\n"); return NOSTR_SUCCESS; // Default allow if no restrictive rules matched } // Removed unused functions: validate_nip42_event, validate_admin_event //============================================================================= // NIP-42 CHALLENGE MANAGEMENT FUNCTIONS //============================================================================= /** * Generate a challenge ID using nostr_core_lib */ static int generate_challenge_id(char *challenge_buffer, size_t buffer_size) { if (!challenge_buffer || buffer_size < 65) { return NOSTR_ERROR_INVALID_INPUT; } // Use nostr_core_lib to generate a random challenge return nostr_nip42_generate_challenge(challenge_buffer, 32); } /** * Clean up expired challenges from memory */ static void cleanup_expired_challenges(void) { time_t now = time(NULL); // Only cleanup if enough time has passed if (now - g_challenge_manager.last_cleanup < CHALLENGE_CLEANUP_INTERVAL) { return; } int active_count = 0; for (int i = 0; i < g_challenge_manager.challenge_count; i++) { if (g_challenge_manager.challenges[i].active) { if (now > g_challenge_manager.challenges[i].expires_at) { // Mark expired challenge as inactive g_challenge_manager.challenges[i].active = 0; memset(g_challenge_manager.challenges[i].challenge_id, 0, sizeof(g_challenge_manager.challenges[i].challenge_id)); } else { active_count++; } } } // Compact the array if we have many inactive entries if (active_count < g_challenge_manager.challenge_count / 2 && active_count < MAX_CHALLENGES - 100) { int write_idx = 0; for (int read_idx = 0; read_idx < g_challenge_manager.challenge_count; read_idx++) { if (g_challenge_manager.challenges[read_idx].active) { if (write_idx != read_idx) { memcpy(&g_challenge_manager.challenges[write_idx], &g_challenge_manager.challenges[read_idx], sizeof(nip42_challenge_entry_t)); } write_idx++; } } g_challenge_manager.challenge_count = write_idx; } g_challenge_manager.last_cleanup = now; char cleanup_msg[256]; sprintf(cleanup_msg, "NIP-42: Cleaned up challenges, %d active remaining\n", active_count); validator_debug_log(cleanup_msg); } /** * Store a new challenge in memory */ static int store_challenge(const char *challenge_id, const char *client_ip) { if (!challenge_id || strlen(challenge_id) == 0) { return NOSTR_ERROR_INVALID_INPUT; } cleanup_expired_challenges(); // Find an available slot int slot_idx = -1; // First, try to find an inactive slot for (int i = 0; i < g_challenge_manager.challenge_count; i++) { if (!g_challenge_manager.challenges[i].active) { slot_idx = i; break; } } // If no inactive slot found, use next available if we haven't hit max if (slot_idx == -1 && g_challenge_manager.challenge_count < MAX_CHALLENGES) { slot_idx = g_challenge_manager.challenge_count++; } // If still no slot, we're full - remove oldest entry if (slot_idx == -1) { slot_idx = 0; // Overwrite first entry (oldest) } // Store the new challenge nip42_challenge_entry_t *entry = &g_challenge_manager.challenges[slot_idx]; memset(entry, 0, sizeof(nip42_challenge_entry_t)); // Store challenge with proper length handling (up to buffer size - 1) strncpy(entry->challenge_id, challenge_id, sizeof(entry->challenge_id) - 1); entry->challenge_id[sizeof(entry->challenge_id) - 1] = '\0'; if (client_ip) { strncpy(entry->client_ip, client_ip, sizeof(entry->client_ip) - 1); entry->client_ip[sizeof(entry->client_ip) - 1] = '\0'; } time_t now = time(NULL); entry->created_at = now; entry->expires_at = now + g_challenge_manager.timeout_seconds; entry->active = 1; char store_msg[256]; sprintf(store_msg, "NIP-42: Stored challenge %.16s... (expires in %d seconds)\n", challenge_id, g_challenge_manager.timeout_seconds); validator_debug_log(store_msg); return NOSTR_SUCCESS; } /** * Generate and store a new NIP-42 challenge for /auth endpoint */ int nostr_generate_nip42_challenge(char *challenge_out, size_t challenge_size, const char *client_ip) { if (!challenge_out || challenge_size < 65) { return NOSTR_ERROR_INVALID_INPUT; } // Generate challenge ID int result = generate_challenge_id(challenge_out, challenge_size); if (result != NOSTR_SUCCESS) { return result; } // Store in challenge manager result = store_challenge(challenge_out, client_ip); if (result != NOSTR_SUCCESS) { return result; } return NOSTR_SUCCESS; } //============================================================================= // SIMPLE NIP-42 FUNCTION IMPLEMENTATIONS FOR C-RELAY //============================================================================= /** * Simple NIP-42 challenge generation (generates random hex string) */ int nostr_nip42_generate_challenge(char *challenge_buffer, size_t buffer_size) { if (!challenge_buffer || buffer_size < 65) { return NOSTR_ERROR_INVALID_INPUT; } // Generate 32 random bytes and convert to hex string unsigned char random_bytes[32]; // Simple random number generation using time and rand() srand((unsigned int)time(NULL)); for (int i = 0; i < 32; i++) { random_bytes[i] = (unsigned char)(rand() % 256); } // Convert to hex string for (int i = 0; i < 32; i++) { sprintf(&challenge_buffer[i * 2], "%02x", random_bytes[i]); } challenge_buffer[64] = '\0'; return NOSTR_SUCCESS; } /** * Simple NIP-42 auth event verification (validates basic structure and expiration) */ int nostr_nip42_verify_auth_event(cJSON *event, const char *challenge_id, const char *relay_url, int time_tolerance_seconds) { if (!event || !challenge_id || !relay_url) { return NOSTR_ERROR_INVALID_INPUT; } // Check if event has the required tags for NIP-42 cJSON *tags = cJSON_GetObjectItem(event, "tags"); if (!tags || !cJSON_IsArray(tags)) { return NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID; } int has_relay_tag = 0; int has_challenge_tag = 0; time_t expiration = 0; // Look for required tags cJSON *tag = NULL; cJSON_ArrayForEach(tag, tags) { if (!cJSON_IsArray(tag)) continue; cJSON *tag_name = cJSON_GetArrayItem(tag, 0); if (!tag_name || !cJSON_IsString(tag_name)) continue; const char *tag_name_str = cJSON_GetStringValue(tag_name); if (strcmp(tag_name_str, "relay") == 0) { has_relay_tag = 1; cJSON *relay_value = cJSON_GetArrayItem(tag, 1); if (relay_value && cJSON_IsString(relay_value)) { const char *event_relay = cJSON_GetStringValue(relay_value); // For C-relay, accept the relay URL as valid (basic check) if (event_relay && strlen(event_relay) > 0) { // Relay URL validation passed } } } else if (strcmp(tag_name_str, "challenge") == 0) { has_challenge_tag = 1; cJSON *challenge_value = cJSON_GetArrayItem(tag, 1); if (challenge_value && cJSON_IsString(challenge_value)) { const char *event_challenge = cJSON_GetStringValue(challenge_value); if (!event_challenge || strcmp(event_challenge, challenge_id) != 0) { return NOSTR_ERROR_NIP42_INVALID_CHALLENGE; } } } else if (strcmp(tag_name_str, "expiration") == 0) { cJSON *exp_value = cJSON_GetArrayItem(tag, 1); if (exp_value && cJSON_IsString(exp_value)) { const char *exp_str = cJSON_GetStringValue(exp_value); // Validate that the expiration string contains only digits (and optional leading whitespace) const char* p = exp_str; // Skip leading whitespace while (*p == ' ' || *p == '\t') p++; // Check if we have at least one digit and all remaining characters are digits if (*p != '\0') { const char* digit_start = p; while (*p >= '0' && *p <= '9') p++; // If we consumed the entire string and found at least one digit, it's valid if (*p == '\0' && p > digit_start) { expiration = (time_t)atol(exp_str); } // If malformed, expiration remains 0 (no expiration), effectively ignoring the tag } } } } // Check required tags are present if (!has_relay_tag || !has_challenge_tag) { return NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID; } // Check expiration with time tolerance time_t now = time(NULL); if (expiration > 0) { if (now > expiration + time_tolerance_seconds) { return NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED; } } // Check created_at timestamp for reasonable bounds cJSON *created_at_json = cJSON_GetObjectItem(event, "created_at"); if (created_at_json && cJSON_IsNumber(created_at_json)) { time_t created_at = (time_t)cJSON_GetNumberValue(created_at_json); if (abs((int)(now - created_at)) > time_tolerance_seconds) { return NOSTR_ERROR_NIP42_TIME_TOLERANCE; } } return NOSTR_SUCCESS; }