/* * 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 "debug.h" // C-relay debug system #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 - already defined in nostr_common.h) // 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; //============================================================================= // GLOBAL STATE //============================================================================= // No longer using local auth cache - using unified cache from config.c 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}; //============================================================================= // FORWARD DECLARATIONS //============================================================================= static int reload_auth_config(void); // Removed unused forward declarations for functions that are no longer called 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) { return NOSTR_ERROR_CRYPTO_INIT; } // Load initial configuration from database int result = reload_auth_config(); if (result != NOSTR_SUCCESS) { return result; } // Initialize NIP-42 challenge manager using unified config memset(&g_challenge_manager, 0, sizeof(g_challenge_manager)); const char* nip42_timeout = get_config_value("nip42_challenge_timeout"); g_challenge_manager.timeout_seconds = nip42_timeout ? atoi(nip42_timeout) : 600; if (nip42_timeout) free((char*)nip42_timeout); const char* nip42_tolerance = get_config_value("nip42_time_tolerance"); g_challenge_manager.time_tolerance_seconds = nip42_tolerance ? atoi(nip42_tolerance) : 300; if (nip42_tolerance) free((char*)nip42_tolerance); g_challenge_manager.last_cleanup = time(NULL); g_validator_initialized = 1; return NOSTR_SUCCESS; } /** * Check if authentication rules are enabled */ int nostr_auth_rules_enabled(void) { // Use unified cache from config.c const char* auth_enabled = get_config_value("auth_enabled"); int result = 0; if (auth_enabled && strcmp(auth_enabled, "true") == 0) { result = 1; } if (auth_enabled) free((char*)auth_enabled); // Also check legacy key const char* auth_rules_enabled = get_config_value("auth_rules_enabled"); if (auth_rules_enabled && strcmp(auth_rules_enabled, "true") == 0) { result = 1; } if (auth_rules_enabled) free((char*)auth_rules_enabled); return result; } /////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////// // 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) { return NOSTR_ERROR_INVALID_INPUT; } // 2. Initialization Check - Verify system is properly initialized if (!g_validator_initialized) { return NOSTR_ERROR_INVALID_INPUT; } // 3. Parse JSON string to cJSON event object cJSON *event = cJSON_ParseWithLength(json_string, json_length); if (!event) { 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)) { cJSON_Delete(event); return NOSTR_ERROR_INVALID_INPUT; } int event_kind = (int)cJSON_GetNumberValue(kind); // 5. Check configuration using unified cache int auth_required = nostr_auth_rules_enabled(); ///////////////////////////////////////////////////////////////////// // 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) { cJSON_Delete(event); return validation_result; } // 7. Extract pubkey for rule evaluation const char *event_pubkey = cJSON_GetStringValue(pubkey); if (!event_pubkey || strlen(event_pubkey) != 64) { cJSON_Delete(event); return NOSTR_ERROR_EVENT_INVALID_PUBKEY; } ///////////////////////////////////////////////////////////////////// // PHASE 3: ADMIN EVENT BYPASS CHECK ///////////////////////////////////////////////////////////////////// // 8. Check if this is a kind 23456 admin event from authorized admin // This must happen AFTER signature validation but BEFORE auth rules if (event_kind == 23456) { const char* admin_pubkey = get_config_value("admin_pubkey"); if (admin_pubkey && strcmp(event_pubkey, admin_pubkey) == 0) { // Valid admin event - bypass remaining validation cJSON_Delete(event); return NOSTR_SUCCESS; } // Not from admin - continue with normal validation } ///////////////////////////////////////////////////////////////////// // PHASE 4: EVENT KIND SPECIFIC VALIDATION ///////////////////////////////////////////////////////////////////// // 9. Handle NIP-42 authentication challenge events (kind 22242) if (event_kind == 22242) { // Check NIP-42 mode using unified cache const char* nip42_enabled = get_config_value("nip42_auth_enabled"); if (nip42_enabled && strcmp(nip42_enabled, "false") == 0) { free((char*)nip42_enabled); cJSON_Delete(event); return NOSTR_ERROR_NIP42_DISABLED; } if (nip42_enabled) free((char*)nip42_enabled); // TODO: Implement full NIP-42 challenge validation // For now, accept all valid NIP-42 events cJSON_Delete(event); return NOSTR_SUCCESS; } ///////////////////////////////////////////////////////////////////// // PHASE 5: AUTHENTICATION RULES (Database Queries) ///////////////////////////////////////////////////////////////////// // 10. Check if authentication rules are enabled if (!auth_required) { } else { // 11. Check database authentication rules (only if auth enabled) // 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) { cJSON_Delete(event); return rules_result; } } } ///////////////////////////////////////////////////////////////////// // PHASE 6: ADDITIONAL VALIDATIONS (C-relay specific) ///////////////////////////////////////////////////////////////////// // 12. NIP-13 Proof of Work validation int pow_enabled = get_config_bool("pow_enabled", 0); int pow_min_difficulty = get_config_int("pow_min_difficulty", 0); int pow_validation_flags = get_config_int("pow_validation_flags", 1); if (pow_enabled && pow_min_difficulty > 0) { nostr_pow_result_t pow_result; int pow_validation_result = nostr_validate_pow(event, pow_min_difficulty, pow_validation_flags, &pow_result); if (pow_validation_result != NOSTR_SUCCESS) { cJSON_Delete(event); return pow_validation_result; } } // 13. NIP-40 Expiration validation // Always check expiration tags if present (following NIP-40 specification) 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') { } 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) { } 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) { cJSON_Delete(event); return NOSTR_ERROR_EVENT_EXPIRED; } } } } // All validations passed 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; 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 //============================================================================= /** * Force cache refresh - cache no longer exists, function kept for compatibility */ void nostr_request_validator_force_cache_refresh(void) { // Cache no longer exists - direct database queries are used } /** * This function is no longer needed - configuration is handled by unified cache */ static int reload_auth_config(void) { // Configuration is now handled by the unified cache in config.c 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 */ int check_database_auth_rules(const char *pubkey, const char *operation __attribute__((unused)), const char *resource_hash) { sqlite3 *db = NULL; sqlite3_stmt *stmt = NULL; int rc; DEBUG_TRACE("Checking auth rules for pubkey: %s", pubkey); if (!pubkey) { return NOSTR_ERROR_INVALID_INPUT; } // Open database using global database path if (strlen(g_database_path) == 0) { return NOSTR_SUCCESS; // Default allow on DB error } rc = sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc != SQLITE_OK) { return NOSTR_SUCCESS; // Default allow on DB error } // Step 1: Check pubkey blacklist (highest priority) const char *blacklist_sql = "SELECT rule_type FROM auth_rules WHERE rule_type = " "'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1"; DEBUG_TRACE("Blacklist SQL: %s", blacklist_sql); rc = sqlite3_prepare_v2(db, blacklist_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC); int step_result = sqlite3_step(stmt); DEBUG_TRACE("Blacklist query result: %s", step_result == SQLITE_ROW ? "FOUND" : "NOT_FOUND"); if (step_result == SQLITE_ROW) { DEBUG_TRACE("BLACKLIST HIT: Denying access for pubkey: %s", pubkey); // Set specific violation details for status code mapping strcpy(g_last_rule_violation.violation_type, "pubkey_blacklist"); sprintf(g_last_rule_violation.reason, "Public key blacklisted"); sqlite3_finalize(stmt); sqlite3_close(db); return NOSTR_ERROR_AUTH_REQUIRED; } sqlite3_finalize(stmt); } // Step 2: Check hash blacklist if (resource_hash) { const char *hash_blacklist_sql = "SELECT rule_type FROM auth_rules WHERE rule_type = " "'blacklist' AND pattern_type = 'hash' AND pattern_value = ? AND active = 1 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); if (sqlite3_step(stmt) == SQLITE_ROW) { // Set specific violation details for status code mapping strcpy(g_last_rule_violation.violation_type, "hash_blacklist"); sprintf(g_last_rule_violation.reason, "File hash blacklisted"); sqlite3_finalize(stmt); sqlite3_close(db); return NOSTR_ERROR_AUTH_REQUIRED; } sqlite3_finalize(stmt); } } // Step 3: Check pubkey whitelist const char *whitelist_sql = "SELECT rule_type FROM auth_rules WHERE rule_type = " "'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 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); if (sqlite3_step(stmt) == SQLITE_ROW) { sqlite3_finalize(stmt); sqlite3_close(db); return NOSTR_SUCCESS; // Allow whitelisted pubkey } sqlite3_finalize(stmt); } // 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 = 'whitelist' " "AND pattern_type = 'pubkey' AND active = 1 LIMIT 1"; rc = sqlite3_prepare_v2(db, whitelist_exists_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { int whitelist_count = sqlite3_column_int(stmt, 0); if (whitelist_count > 0) { // 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); } sqlite3_close(db); 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; } /** * 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; 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; }