/* * Ginxsom 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 ginxsom-specific 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/nip042.h" #include "../nostr_core_lib/nostr_core/nostr_common.h" #include "../nostr_core_lib/nostr_core/utils.h" #include "ginxsom.h" #include #include #include #include #include #include // 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 // Use global database path from main.c extern char g_db_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 our debug.log file */ static void validator_debug_log(const char *message) { FILE *debug_log = fopen("logs/app/debug.log", "a"); if (debug_log) { fprintf(debug_log, "%ld %s", (long)time(NULL), message); fclose(debug_log); } } //============================================================================= // FORWARD DECLARATIONS //============================================================================= static int reload_auth_config(void); static int parse_authorization_header(const char *auth_header, char *event_json, size_t json_size); static int extract_pubkey_from_event(cJSON *event, char *pubkey_buffer, size_t buffer_size); static int validate_blossom_event(cJSON *event, const char *expected_hash, const char *method); static int validate_nip42_event(cJSON *event, const char *relay_url, const char *challenge_id); static int validate_admin_event(cJSON *event, const char *method, const char *endpoint); static int check_database_auth_rules(const char *pubkey, const char *operation, const char *resource_hash, const char *mime_type); 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 validate_challenge(const char *challenge_id); 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 nostr_unified_request_t *request, nostr_request_result_t *result) { // 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 (!request || !result) { return NOSTR_ERROR_INVALID_INPUT; } // 2. Initialization Check - Verify system is properly initialized if (!g_validator_initialized) { return NOSTR_ERROR_INVALID_INPUT; } // 3. Basic Structure Validation - Ensure required fields are present // Initialize result structure memset(result, 0, sizeof(nostr_request_result_t)); result->valid = 1; // Default allow result->error_code = NOSTR_SUCCESS; strcpy(result->reason, "No validation required"); result->file_data = NULL; result->file_size = 0; result->owns_file_data = 0; memset(result->expected_hash, 0, sizeof(result->expected_hash)); // 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 3 PASSED - Configuration loaded " "(auth_required=%d, nip42_enabled=%d)\n", g_auth_cache.auth_required, request->nip42_enabled); // Handle challenge generation operation (no authentication required) if (request->operation && strcmp(request->operation, "challenge") == 0) { // Check if NIP-42 is enabled if (!request->nip42_enabled || g_auth_cache.nip42_mode == 0) { result->valid = 0; result->error_code = NOSTR_ERROR_NIP42_DISABLED; strcpy(result->reason, "NIP-42 authentication is disabled"); return NOSTR_SUCCESS; } // Generate and store challenge char challenge_id[65]; int challenge_result = generate_challenge_id(challenge_id, sizeof(challenge_id)); if (challenge_result != NOSTR_SUCCESS) { result->valid = 0; result->error_code = challenge_result; strcpy(result->reason, "Failed to generate challenge ID"); return NOSTR_SUCCESS; } // Store challenge in manager int store_result = store_challenge(challenge_id, request->client_ip); if (store_result != NOSTR_SUCCESS) { result->valid = 0; result->error_code = store_result; strcpy(result->reason, "Failed to store challenge"); return NOSTR_SUCCESS; } // Return challenge in result (we'll use the reason field for the challenge // ID) snprintf(result->reason, sizeof(result->reason), "CHALLENGE:%s", challenge_id); result->valid = 1; result->error_code = NOSTR_SUCCESS; char challenge_msg[256]; sprintf(challenge_msg, "VALIDATOR_DEBUG: STEP 4 PASSED - Challenge generated: %.16s...\n", challenge_id); return NOSTR_SUCCESS; } ///////////////////////////////////////////////////////////////////// // PHASE 2: NOSTR EVENT VALIDATION (CPU Intensive ~2ms) ///////////////////////////////////////////////////////////////////// // Check if authentication is disabled first (regardless of header presence) if (!g_auth_cache.auth_required) { validator_debug_log("VALIDATOR_DEBUG: STEP 4 PASSED - Authentication " "disabled, allowing request\n"); result->valid = 1; result->error_code = NOSTR_SUCCESS; strcpy(result->reason, "Authentication disabled"); return NOSTR_SUCCESS; } // Check if this is a BUD-09 report request - allow anonymous reporting if (request->operation && strcmp(request->operation, "report") == 0) { // BUD-09 allows anonymous reporting - pass through to bud09.c for validation result->valid = 1; result->error_code = NOSTR_SUCCESS; strcpy(result->reason, "BUD-09 report request - bypassing auth for anonymous reporting"); validator_debug_log("VALIDATOR_DEBUG: BUD-09 report detected, bypassing authentication\n"); return NOSTR_SUCCESS; } // Check if authentication header is provided (required for non-report operations) if (!request->auth_header) { result->valid = 0; result->error_code = NOSTR_ERROR_AUTH_REQUIRED; strcpy(result->reason, "Authentication required but not provided"); return NOSTR_SUCCESS; } char header_msg[110]; sprintf(header_msg, "VALIDATOR_DEBUG: STEP 4 PASSED - Auth header provided: %.50s...\n", request->auth_header); // 4. Authorization Header Parsing - Extract base64-encoded Nostr event // Format: "Authorization: Nostr " // Early exit: Invalid base64 or malformed header rejected immediately char event_json[4096]; int parse_result = parse_authorization_header(request->auth_header, event_json, sizeof(event_json)); if (parse_result != NOSTR_SUCCESS) { char parse_msg[256]; sprintf(parse_msg, "VALIDATOR_DEBUG: STEP 5 FAILED - Failed to parse authorization " "header (error=%d)\n", parse_result); result->valid = 0; result->error_code = parse_result; strcpy(result->reason, "Invalid authorization header format. Must be 'Nostr '"); return NOSTR_SUCCESS; } char parse_success_msg[512]; sprintf(parse_success_msg, "VALIDATOR_DEBUG: STEP 5 PASSED - Authorization header parsed, JSON: " "%.100s...\n", event_json); // 5. JSON Parsing - Parse Nostr event JSON using cJSON // Early exit: Invalid JSON rejected before signature verification cJSON *event = cJSON_Parse(event_json); if (!event) { result->valid = 0; result->error_code = NOSTR_ERROR_EVENT_INVALID_CONTENT; strcpy(result->reason, "Invalid JSON in authorization header. Ensure event is properly formatted JSON."); return NOSTR_SUCCESS; } // 6. Nostr Event Structure Validation - Validate required fields // Early exit: Invalid structure rejected before expensive crypto // operations int validation_result = nostr_validate_event(event); if (validation_result != NOSTR_SUCCESS) { char validation_msg[256]; sprintf(validation_msg, "VALIDATOR_DEBUG: STEP 7 FAILED - NOSTR event validation failed " "(error=%d)\n", validation_result); result->valid = 0; result->error_code = validation_result; // Map event validation errors to detailed messages switch (validation_result) { case NOSTR_ERROR_EVENT_INVALID_STRUCTURE: strcpy(result->reason, "Event structure invalid. Missing required fields: id, pubkey, created_at, kind, tags, content, sig"); break; case NOSTR_ERROR_EVENT_INVALID_ID: strcpy(result->reason, "Event ID verification failed. Check event serialization and hash calculation."); break; case NOSTR_ERROR_EVENT_INVALID_PUBKEY: strcpy(result->reason, "Invalid pubkey format. Must be 64-character hex string."); break; case NOSTR_ERROR_EVENT_INVALID_SIGNATURE: strcpy(result->reason, "Event signature verification failed. Check private key and signing process."); break; case NOSTR_ERROR_EVENT_INVALID_CREATED_AT: strcpy(result->reason, "Invalid created_at timestamp. Must be valid Unix timestamp."); break; case NOSTR_ERROR_EVENT_INVALID_KIND: strcpy(result->reason, "Invalid event kind. Must be valid integer."); break; case NOSTR_ERROR_EVENT_INVALID_TAGS: strcpy(result->reason, "Invalid tags format. Tags must be array of string arrays."); break; case NOSTR_ERROR_EVENT_INVALID_CONTENT: strcpy(result->reason, "Invalid content format. Content must be valid string."); break; default: snprintf(result->reason, sizeof(result->reason), "NOSTR event validation failed (error code: %d). Check event structure and format.", validation_result); break; } cJSON_Delete(event); return NOSTR_SUCCESS; } // 11. Public Key Extraction (Both Paths) // Extract validated public key for rule evaluation char extracted_pubkey[65] = {0}; int extract_result = extract_pubkey_from_event(event, extracted_pubkey, sizeof(extracted_pubkey)); if (extract_result != NOSTR_SUCCESS) { char extract_msg[256]; sprintf(extract_msg, "VALIDATOR_DEBUG: STEP 8 FAILED - Failed to extract pubkey from " "event (error=%d)\n", extract_result); result->valid = 0; result->error_code = extract_result; strcpy(result->reason, "Failed to extract public key from event. Pubkey must be 64-character hex string."); cJSON_Delete(event); return NOSTR_SUCCESS; } char extract_success_msg[256]; sprintf(extract_success_msg, "VALIDATOR_DEBUG: STEP 8 PASSED - Extracted pubkey: %s\n", extracted_pubkey); ///////////////////////////////////////////////////////////////////// // EVENT KIND ROUTING - Dual Authentication Support ///////////////////////////////////////////////////////////////////// // Kind 22242 (NIP-42): Route to NIP-42 challenge validation // Kind 24242 (Blossom): Route to Blossom operation validation // Other Kinds: Skip Nostr validation, proceed to rules // Invalid Kind: Reject immediately // Get event kind to determine authentication method cJSON *kind_json = cJSON_GetObjectItem(event, "kind"); int event_kind = 0; if (kind_json && cJSON_IsNumber(kind_json)) { event_kind = cJSON_GetNumberValue(kind_json); } char kind_msg[256]; sprintf(kind_msg, "VALIDATOR_DEBUG: STEP 9 PASSED - Event kind: %d\n", event_kind); // Variable to store expected hash from Blossom events for Phase 4 validation char expected_hash_from_event[65] = {0}; ///////////////////////////////////////////////////////////////////// // NIP42 ///////////////////////////////////////////////////////////////////// if (event_kind == NOSTR_NIP42_AUTH_EVENT_KIND) { // 8. NIP-42 Challenge Validation (Kind 22242 Only ~500μs) // Validate relay tag, verify challenge tag, check expiration char nip42_msg[256]; sprintf(nip42_msg, "VALIDATOR_DEBUG: STEP 10 - Processing NIP-42 authentication (kind " "%d)\n", NOSTR_NIP42_AUTH_EVENT_KIND); validator_debug_log(nip42_msg); // NIP-42 authentication (kind 22242) if (!request->nip42_enabled || g_auth_cache.nip42_mode == 0) { validator_debug_log("VALIDATOR_DEBUG: STEP 10 FAILED - NIP-42 " "authentication is disabled\n"); result->valid = 0; result->error_code = NOSTR_ERROR_NIP42_DISABLED; strcpy(result->reason, "NIP-42 authentication is disabled"); cJSON_Delete(event); return NOSTR_SUCCESS; } // Extract challenge from event according to NIP-42 spec (tags only) const char *challenge_for_validation = request->challenge_id; if (!challenge_for_validation) { // Look for challenge in tags (NIP-42 spec compliant) cJSON *tags_json = cJSON_GetObjectItem(event, "tags"); if (tags_json && cJSON_IsArray(tags_json)) { cJSON *tag = NULL; cJSON_ArrayForEach(tag, tags_json) { 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, "challenge") == 0) { cJSON *challenge_value = cJSON_GetArrayItem(tag, 1); if (challenge_value && cJSON_IsString(challenge_value)) { const char *challenge_from_tag = cJSON_GetStringValue(challenge_value); if (challenge_from_tag && strlen(challenge_from_tag) > 0) { // NIP-42 doesn't specify a fixed length, so accept any reasonable length size_t challenge_len = strlen(challenge_from_tag); if (challenge_len >= 8 && challenge_len <= 128) { // Reasonable bounds // Basic validation - should be hex-like int valid_chars = 1; for (size_t i = 0; i < challenge_len; i++) { char c = challenge_from_tag[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { valid_chars = 0; break; } } if (valid_chars) { challenge_for_validation = challenge_from_tag; char extract_msg[256]; sprintf(extract_msg, "VALIDATOR_DEBUG: STEP 10 - Extracted challenge from event " "tag: %.16s...\n", challenge_for_validation); validator_debug_log(extract_msg); break; // Found it, stop looking } } } } } } } } if (!request->request_url || !challenge_for_validation) { validator_debug_log( "VALIDATOR_DEBUG: STEP 10 FAILED - NIP-42 requires request_url and " "challenge (from event tags)\n"); result->valid = 0; result->error_code = NOSTR_ERROR_NIP42_INVALID_CHALLENGE; strcpy(result->reason, "NIP-42 authentication requires request_url and challenge in event tags"); cJSON_Delete(event); return NOSTR_SUCCESS; } int nip42_result = validate_nip42_event(event, request->request_url, challenge_for_validation); if (nip42_result != NOSTR_SUCCESS) { char nip42_fail_msg[256]; sprintf(nip42_fail_msg, "VALIDATOR_DEBUG: STEP 10 FAILED - NIP-42 validation failed " "(error=%d)\n", nip42_result); validator_debug_log(nip42_fail_msg); result->valid = 0; result->error_code = nip42_result; // Map specific NIP-42 error codes to detailed error messages switch (nip42_result) { case NOSTR_ERROR_NIP42_INVALID_CHALLENGE: strcpy(result->reason, "Challenge not found or invalid. Request a new challenge from /auth endpoint."); break; case NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED: strcpy(result->reason, "Challenge has expired. Request a new challenge from /auth endpoint."); break; case NOSTR_ERROR_NIP42_URL_MISMATCH: strcpy(result->reason, "Relay URL in auth event does not match server. Use 'ginxsom' as relay value."); break; case NOSTR_ERROR_NIP42_TIME_TOLERANCE: strcpy(result->reason, "Auth event timestamp is outside acceptable time window. Check system clock."); break; case NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID: strcpy(result->reason, "NIP-42 auth event structure is invalid. Verify event format and required tags."); break; case NOSTR_ERROR_EVENT_INVALID_SIGNATURE: strcpy(result->reason, "Event signature verification failed. Check private key and event serialization."); break; case NOSTR_ERROR_EVENT_INVALID_CONTENT: strcpy(result->reason, "Event content is invalid. Challenge must be in event tags according to NIP-42."); break; case NOSTR_ERROR_EVENT_INVALID_TAGS: strcpy(result->reason, "Required tags missing. Auth event must include 'relay' and 'expiration' tags."); break; default: snprintf(result->reason, sizeof(result->reason), "NIP-42 authentication failed (error code: %d). Check event structure and signature.", nip42_result); break; } cJSON_Delete(event); return NOSTR_SUCCESS; } validator_debug_log( "VALIDATOR_DEBUG: STEP 10 PASSED - NIP-42 authentication succeeded\n"); strcpy(result->reason, "NIP-42 authentication passed"); } else if (event_kind == 24242) { // 10. Operation-Specific Validation (Kind 24242 Only) // Verify operation authorization, check required tags, validate // expiration Early exit: Expired or mismatched events rejected validator_debug_log("VALIDATOR_DEBUG: STEP 10 - Processing Blossom " "authentication (kind 24242)\n"); // Blossom protocol authentication (kind 24242) - ALWAYS validate kind 24242 events char blossom_valid_msg[512]; sprintf(blossom_valid_msg, "VALIDATOR_DEBUG: Validating Blossom event for operation='%s', " "hash='%s'\n", request->operation ? request->operation : "NULL", request->resource_hash ? request->resource_hash : "NULL"); validator_debug_log(blossom_valid_msg); int blossom_result = validate_blossom_event(event, request->resource_hash, request->operation); if (blossom_result != NOSTR_SUCCESS) { char blossom_fail_msg[256]; sprintf(blossom_fail_msg, "VALIDATOR_DEBUG: STEP 10 FAILED - Blossom validation failed " "(error=%d)\n", blossom_result); validator_debug_log(blossom_fail_msg); result->valid = 0; result->error_code = blossom_result; // Map specific Blossom error codes to detailed error messages switch (blossom_result) { case NOSTR_ERROR_EVENT_EXPIRED: strcpy(result->reason, "Authorization event has expired. Create a new signed event with future expiration."); break; case NOSTR_ERROR_EVENT_INVALID_CONTENT: strcpy(result->reason, "Event missing required tags. Blossom events need 't' (method) and 'x' tags."); break; case NOSTR_ERROR_EVENT_INVALID_TAGS: strcpy(result->reason, "Invalid or missing Blossom tags. Check 't' tag matches operation and 'x' tag matches file hash."); break; case NOSTR_ERROR_EVENT_INVALID_SIGNATURE: strcpy(result->reason, "Event signature verification failed. Check private key and event serialization."); break; case NOSTR_ERROR_EVENT_INVALID_KIND: strcpy(result->reason, "Invalid event kind. Blossom authorization events must use kind 24242."); break; default: snprintf(result->reason, sizeof(result->reason), "Blossom event does not authorize this operation (error code: %d). Check tags and expiration.", blossom_result); break; } cJSON_Delete(event); return NOSTR_SUCCESS; } // Extract expected hash from event 'x' tag for Phase 4 use cJSON *tags = cJSON_GetObjectItem(event, "tags"); if (tags && cJSON_IsArray(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, "x") == 0) { cJSON *hash_value = cJSON_GetArrayItem(tag, 1); if (hash_value && cJSON_IsString(hash_value)) { const char *event_hash = cJSON_GetStringValue(hash_value); if (event_hash && strlen(event_hash) == 64) { strncpy(expected_hash_from_event, event_hash, 64); expected_hash_from_event[64] = '\0'; break; } } } } } validator_debug_log( "VALIDATOR_DEBUG: STEP 10 PASSED - Blossom authentication succeeded\n"); strcpy(result->reason, "Blossom authentication passed"); } else if (event_kind == 33335) { // 10. Admin/Configuration Event Validation (Kind 33335) // Verify admin authorization, check required tags, validate expiration validator_debug_log("VALIDATOR_DEBUG: STEP 10 - Processing Admin/Configuration " "authentication (kind 33335)\n"); char admin_valid_msg[512]; sprintf(admin_valid_msg, "VALIDATOR_DEBUG: Validating Admin event for operation='%s', " "endpoint='%s'\n", request->operation ? request->operation : "NULL", request->resource_hash ? request->resource_hash : "admin_api"); validator_debug_log(admin_valid_msg); // For admin operations, we pass the HTTP method and API endpoint const char *admin_method = NULL; const char *admin_endpoint = NULL; // Extract method and endpoint from request context // For admin API, we need to get the actual HTTP method and full endpoint if (request->operation && strcmp(request->operation, "admin") == 0) { // Admin events should contain method and endpoint tags that match the actual request // We don't enforce specific values here - let the event tags drive the validation admin_method = NULL; // Let validation check event tags without enforcing specific method admin_endpoint = NULL; // Let validation check event tags without enforcing specific endpoint } int admin_result = validate_admin_event(event, admin_method, admin_endpoint); if (admin_result != NOSTR_SUCCESS) { char admin_fail_msg[256]; sprintf(admin_fail_msg, "VALIDATOR_DEBUG: STEP 10 FAILED - Admin validation failed " "(error=%d)\n", admin_result); validator_debug_log(admin_fail_msg); result->valid = 0; result->error_code = admin_result; // Map specific Admin error codes to detailed error messages switch (admin_result) { case NOSTR_ERROR_EVENT_EXPIRED: strcpy(result->reason, "Admin authorization event has expired. Create a new signed event with future expiration."); break; case NOSTR_ERROR_EVENT_INVALID_CONTENT: strcpy(result->reason, "Admin event missing required tags. Admin events need 'method' and 'endpoint' tags."); break; case NOSTR_ERROR_EVENT_INVALID_TAGS: strcpy(result->reason, "Invalid or missing admin tags. Check 'method' tag matches HTTP method and 'endpoint' tag is valid."); break; case NOSTR_ERROR_EVENT_INVALID_SIGNATURE: strcpy(result->reason, "Admin event signature verification failed. Check private key and event serialization."); break; case NOSTR_ERROR_EVENT_INVALID_KIND: strcpy(result->reason, "Invalid event kind. Admin authorization events must use kind 33335."); break; case NOSTR_ERROR_AUTH_REQUIRED: strcpy(result->reason, "Admin access denied. Pubkey is not authorized for admin operations."); break; default: snprintf(result->reason, sizeof(result->reason), "Admin event does not authorize this operation (error code: %d). Check tags and admin permissions.", admin_result); break; } cJSON_Delete(event); return NOSTR_SUCCESS; } validator_debug_log( "VALIDATOR_DEBUG: STEP 10 PASSED - Admin authentication succeeded\n"); strcpy(result->reason, "Admin authentication passed"); } else { char unsupported_msg[256]; sprintf(unsupported_msg, "VALIDATOR_DEBUG: STEP 10 FAILED - Unsupported event kind: %d\n", event_kind); validator_debug_log(unsupported_msg); result->valid = 0; result->error_code = NOSTR_ERROR_EVENT_INVALID_KIND; snprintf(result->reason, sizeof(result->reason), "Unsupported event kind %d for authentication. Use kind 22242 for NIP-42, kind 24242 for Blossom, or kind 33335 for Admin.", event_kind); cJSON_Delete(event); return NOSTR_SUCCESS; } // Copy validated pubkey to result if (strlen(extracted_pubkey) == 64) { strncpy(result->pubkey, extracted_pubkey, 64); result->pubkey[64] = '\0'; validator_debug_log( "VALIDATOR_DEBUG: STEP 11 PASSED - Pubkey copied to result\n"); } else { char pubkey_warning_msg[256]; sprintf(pubkey_warning_msg, "VALIDATOR_DEBUG: STEP 11 WARNING - Invalid pubkey length: %zu\n", strlen(extracted_pubkey)); validator_debug_log(pubkey_warning_msg); } cJSON_Delete(event); // STEP 12 PASSED: Protocol validation complete - continue to database rule // evaluation validator_debug_log("VALIDATOR_DEBUG: STEP 12 PASSED - Protocol validation " "complete, proceeding to rule evaluation\n"); ///////////////////////////////////////////////////////////////////// // PHASE 3: AUTHENTICATION RULES (Database Queries ~500μs) ///////////////////////////////////////////////////////////////////// // 12. Rules System Check - Quick config check if auth rules enabled // Early exit: If disabled, allow request immediately // Check if authentication is disabled first (regardless of header presence) if (!g_auth_cache.auth_required) { validator_debug_log("VALIDATOR_DEBUG: STEP 4 PASSED - Authentication " "disabled, allowing request\n"); strcpy(result->reason, "Authentication disabled"); return NOSTR_SUCCESS; } // 13. Cache Lookup - Check SQLite cache for previous decision // Early exit: Cache hit returns cached decision (5-minute TTL ~100μs) ///////////////////////////////////////////////////////////////////// // RULE EVALUATION ENGINE (Priority Order) ///////////////////////////////////////////////////////////////////// // a. Pubkey Blacklist (highest priority) - Immediate denial if matched // b. Hash Blacklist - Block specific content hashes // c. MIME Type Blacklist - Block dangerous file types // d. File Size Limits - Enforce upload size restrictions // e. Pubkey Whitelist - Allow specific users (only if not denied above) // f. MIME Type Whitelist - Allow specific file types validator_debug_log("VALIDATOR_DEBUG: STEP 13 PASSED - Auth rules enabled, " "checking database rules\n"); // Check database rules for authorization // For Blossom uploads, use hash from event 'x' tag instead of URI const char *hash_for_rules = request->resource_hash; if (event_kind == 24242 && strlen(expected_hash_from_event) == 64) { hash_for_rules = expected_hash_from_event; char hash_msg[256]; sprintf(hash_msg, "VALIDATOR_DEBUG: Using hash from Blossom event for rules: %.16s...\n", hash_for_rules); validator_debug_log(hash_msg); } int rules_result = check_database_auth_rules( extracted_pubkey, request->operation, hash_for_rules, request->mime_type); if (rules_result != NOSTR_SUCCESS) { validator_debug_log( "VALIDATOR_DEBUG: STEP 14 FAILED - Database rules denied request\n"); result->valid = 0; result->error_code = rules_result; // Determine specific failure reason based on rules evaluation if (rules_result == NOSTR_ERROR_AUTH_REQUIRED) { // This can be pubkey blacklist or whitelist violation - set generic // message The specific reason will be detailed in the database check // function strcpy(result->reason, "Request denied by authorization rules"); } else { strcpy(result->reason, "Authorization error"); } return NOSTR_SUCCESS; } validator_debug_log( "VALIDATOR_DEBUG: STEP 14 PASSED - Database rules allow request\n"); // 15. Whitelist Default Denial - If whitelist rules exist but none matched, // deny // Prevents whitelist bypass attacks // 16. Cache Storage - Store decision for future requests (5-minute TTL) ///////////////////////////////////////////////////////////////////// // PHASE 4: FILE CONTENT VALIDATION (Upload Operations Only ~100ms) ///////////////////////////////////////////////////////////////////// // Only run expensive file processing AFTER all auth checks pass // This ensures blacklisted users never trigger file I/O if (event_kind == 24242 && request->operation && strcmp(request->operation, "upload") == 0 && strlen(expected_hash_from_event) == 64) { validator_debug_log("VALIDATOR_DEBUG: PHASE 4 - Starting file content validation for Blossom upload\n"); char phase4_msg[256]; sprintf(phase4_msg, "VALIDATOR_DEBUG: PHASE 4 - Expected hash: %.16s...\n", expected_hash_from_event); validator_debug_log(phase4_msg); // Get content length from request long content_length = request->file_size; if (content_length <= 0 || content_length > 100 * 1024 * 1024) { // 100MB limit validator_debug_log("VALIDATOR_DEBUG: PHASE 4 FAILED - Invalid content length\n"); result->valid = 0; result->error_code = NOSTR_ERROR_AUTH_REQUIRED; strcpy(result->reason, "Invalid file size for upload validation"); return NOSTR_SUCCESS; } // Allocate buffer for file data unsigned char *file_data = malloc(content_length); if (!file_data) { validator_debug_log("VALIDATOR_DEBUG: PHASE 4 FAILED - Memory allocation failed\n"); result->valid = 0; result->error_code = NOSTR_ERROR_AUTH_REQUIRED; strcpy(result->reason, "Memory allocation failed for file validation"); return NOSTR_SUCCESS; } // Read file data from stdin size_t bytes_read = fread(file_data, 1, content_length, stdin); if (bytes_read != (size_t)content_length) { free(file_data); validator_debug_log("VALIDATOR_DEBUG: PHASE 4 FAILED - Failed to read complete file data\n"); result->valid = 0; result->error_code = NOSTR_ERROR_AUTH_REQUIRED; strcpy(result->reason, "Failed to read complete file data for validation"); return NOSTR_SUCCESS; } char read_msg[256]; sprintf(read_msg, "VALIDATOR_DEBUG: PHASE 4 - Read %zu bytes from stdin\n", bytes_read); validator_debug_log(read_msg); // Calculate SHA-256 hash of file content unsigned char hash_bytes[32]; if (nostr_sha256(file_data, bytes_read, hash_bytes) != NOSTR_SUCCESS) { free(file_data); validator_debug_log("VALIDATOR_DEBUG: PHASE 4 FAILED - Hash calculation failed\n"); result->valid = 0; result->error_code = NOSTR_ERROR_AUTH_REQUIRED; strcpy(result->reason, "Failed to calculate file hash"); return NOSTR_SUCCESS; } // Convert hash to hex string char calculated_hash[65]; nostr_bytes_to_hex(hash_bytes, 32, calculated_hash); char hash_msg[256]; sprintf(hash_msg, "VALIDATOR_DEBUG: PHASE 4 - Calculated hash: %.16s...\n", calculated_hash); validator_debug_log(hash_msg); // Compare hashes if (strcmp(calculated_hash, expected_hash_from_event) != 0) { free(file_data); validator_debug_log("VALIDATOR_DEBUG: PHASE 4 FAILED - Hash mismatch detected\n"); char mismatch_msg[512]; sprintf(mismatch_msg, "VALIDATOR_DEBUG: PHASE 4 - Expected: %.16s..., Got: %.16s...\n", expected_hash_from_event, calculated_hash); validator_debug_log(mismatch_msg); result->valid = 0; result->error_code = NOSTR_ERROR_AUTH_REQUIRED; strcpy(result->reason, "File hash mismatch. Uploaded file does not match hash in authorization event."); return NOSTR_SUCCESS; } // Hash matches - store file data in result for main handler to use result->file_data = file_data; result->file_size = bytes_read; result->owns_file_data = 1; strncpy(result->expected_hash, expected_hash_from_event, 64); result->expected_hash[64] = '\0'; validator_debug_log("VALIDATOR_DEBUG: PHASE 4 PASSED - File hash validation successful, file data stored in result\n"); } // All validations passed result->valid = 1; result->error_code = NOSTR_SUCCESS; validator_debug_log("VALIDATOR_DEBUG: STEP 15 PASSED - All validations " "complete, request ALLOWED\n"); 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 rc = sqlite3_open_v2(g_db_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; } /** * Parse NOSTR authorization header (base64 decode) */ static int parse_authorization_header(const char *auth_header, char *event_json, size_t json_size) { if (!auth_header || !event_json) { return NOSTR_ERROR_INVALID_INPUT; } // Check for "Nostr " prefix (case-insensitive) const char *prefix = "nostr "; size_t prefix_len = strlen(prefix); if (strncasecmp(auth_header, prefix, prefix_len) != 0) { return NOSTR_ERROR_INVALID_INPUT; } // Extract base64 encoded event after "Nostr " const char *base64_event = auth_header + prefix_len; // Decode base64 to JSON using nostr_core_lib base64 decode unsigned char decoded_buffer[4096]; size_t decoded_len = base64_decode(base64_event, decoded_buffer); if (decoded_len == 0 || decoded_len >= json_size) { return NOSTR_ERROR_INVALID_INPUT; } // Copy decoded JSON to output buffer memcpy(event_json, decoded_buffer, decoded_len); event_json[decoded_len] = '\0'; return NOSTR_SUCCESS; } /** * Extract pubkey from validated NOSTR event */ static int extract_pubkey_from_event(cJSON *event, char *pubkey_buffer, size_t buffer_size) { if (!event || !pubkey_buffer || buffer_size < 65) { return NOSTR_ERROR_INVALID_INPUT; } // Initialize buffer to prevent corruption memset(pubkey_buffer, 0, buffer_size); cJSON *pubkey_json = cJSON_GetObjectItem(event, "pubkey"); if (!pubkey_json || !cJSON_IsString(pubkey_json)) { return NOSTR_ERROR_EVENT_INVALID_PUBKEY; } const char *pubkey = cJSON_GetStringValue(pubkey_json); if (!pubkey) { return NOSTR_ERROR_EVENT_INVALID_PUBKEY; } // Check the raw pubkey string before validation size_t pubkey_len = strlen(pubkey); if (pubkey_len != 64) { return NOSTR_ERROR_EVENT_INVALID_PUBKEY; } // Validate that pubkey contains only hex characters before copying for (int i = 0; i < 64; i++) { char c = pubkey[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { return NOSTR_ERROR_EVENT_INVALID_PUBKEY; } } // Safe copy with explicit length and null termination memcpy(pubkey_buffer, pubkey, 64); pubkey_buffer[64] = '\0'; return NOSTR_SUCCESS; } /** * Validate Blossom protocol event (kind 24242) */ static int validate_blossom_event(cJSON *event, const char *expected_hash, const char *method) { if (!event) { return NOSTR_ERROR_INVALID_INPUT; } // Check event kind (must be 24242 for Blossom operations) cJSON *kind_json = cJSON_GetObjectItem(event, "kind"); if (!kind_json || !cJSON_IsNumber(kind_json)) { return NOSTR_ERROR_EVENT_INVALID_CONTENT; } int kind = cJSON_GetNumberValue(kind_json); if (kind != 24242) { return NOSTR_ERROR_EVENT_INVALID_CONTENT; } // ALL Blossom events (kind 24242) must have proper tag structure cJSON *tags = cJSON_GetObjectItem(event, "tags"); if (!tags || !cJSON_IsArray(tags)) { return NOSTR_ERROR_EVENT_INVALID_CONTENT; } // Track what we find in the event tags int has_t_tag = 0; int has_x_tag = 0; int method_matches = (method == NULL); // If no expected method, consider it matched int hash_matches = (expected_hash == NULL); // If no expected hash, consider it matched time_t expiration = 0; 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, "t") == 0) { has_t_tag = 1; cJSON *method_value = cJSON_GetArrayItem(tag, 1); if (method_value && cJSON_IsString(method_value)) { const char *event_method = cJSON_GetStringValue(method_value); if (method && strcmp(event_method, method) == 0) { method_matches = 1; } } } else if (strcmp(tag_name_str, "x") == 0) { has_x_tag = 1; cJSON *hash_value = cJSON_GetArrayItem(tag, 1); if (hash_value && cJSON_IsString(hash_value)) { const char *event_hash = cJSON_GetStringValue(hash_value); if (expected_hash && strcmp(event_hash, expected_hash) == 0) { hash_matches = 1; } } } else if (strcmp(tag_name_str, "expiration") == 0) { cJSON *exp_value = cJSON_GetArrayItem(tag, 1); if (exp_value && cJSON_IsString(exp_value)) { expiration = (time_t)atol(cJSON_GetStringValue(exp_value)); } } } // Blossom events MUST have both 't' and 'x' tags if (!has_t_tag || !has_x_tag) { return NOSTR_ERROR_EVENT_INVALID_CONTENT; } // If we have expected values, they must match if (!method_matches || !hash_matches) { return NOSTR_ERROR_EVENT_INVALID_CONTENT; } // Check expiration time_t now = time(NULL); if (expiration > 0 && now > expiration) { return NOSTR_ERROR_EVENT_EXPIRED; } return NOSTR_SUCCESS; } /** * 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, const char *mime_type) { 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, mime_type=%s\n", pubkey, operation ? operation : "NULL", mime_type ? mime_type : "NULL"); validator_debug_log(rules_msg); // Open database rc = sqlite3_open_v2(g_db_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) // Match both exact operation and wildcard '*' const char *blacklist_sql = "SELECT rule_type, description FROM auth_rules WHERE rule_type = " "'pubkey_blacklist' AND rule_target = ? AND (operation = ? OR 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) { // Match both exact operation and wildcard '*' const char *hash_blacklist_sql = "SELECT rule_type, description FROM auth_rules WHERE rule_type = " "'hash_blacklist' AND rule_target = ? AND (operation = ? OR 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 MIME type blacklist if (mime_type) { // Match both exact MIME type and wildcard patterns (e.g., 'image/*') const char *mime_blacklist_sql = "SELECT rule_type, description FROM auth_rules WHERE rule_type = " "'mime_blacklist' AND (rule_target = ? OR rule_target LIKE '%/*' AND ? LIKE REPLACE(rule_target, '*', '%')) AND (operation = ? OR operation = '*') AND enabled = " "1 ORDER BY priority LIMIT 1"; rc = sqlite3_prepare_v2(db, mime_blacklist_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, mime_type, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, mime_type, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, 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 FAILED - " "MIME type blacklisted\n"); char mime_blacklist_msg[256]; sprintf( mime_blacklist_msg, "VALIDATOR_DEBUG: RULES ENGINE - MIME blacklist rule matched: %s\n", description ? description : "Unknown"); validator_debug_log(mime_blacklist_msg); // Set specific violation details for status code mapping strcpy(g_last_rule_violation.violation_type, "mime_blacklist"); sprintf(g_last_rule_violation.reason, "%s: MIME type blacklisted", description ? description : "TEST_MIME_BLACKLIST"); sqlite3_finalize(stmt); sqlite3_close(db); return NOSTR_ERROR_AUTH_REQUIRED; } sqlite3_finalize(stmt); } validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 PASSED - MIME " "type not blacklisted\n"); } else { validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 SKIPPED - No " "MIME type provided\n"); } // Step 4: Check pubkey whitelist // Match both exact operation and wildcard '*' const char *whitelist_sql = "SELECT rule_type, description FROM auth_rules WHERE rule_type = " "'pubkey_whitelist' AND rule_target = ? AND (operation = ? OR 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 5: Check MIME type whitelist (only if not already denied) if (mime_type) { // Match both exact MIME type and wildcard patterns (e.g., 'image/*') const char *mime_whitelist_sql = "SELECT rule_type, description FROM auth_rules WHERE rule_type = " "'mime_whitelist' AND (rule_target = ? OR rule_target LIKE '%/*' AND ? LIKE REPLACE(rule_target, '*', '%')) AND (operation = ? OR operation = '*') AND enabled = " "1 ORDER BY priority LIMIT 1"; rc = sqlite3_prepare_v2(db, mime_whitelist_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, mime_type, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, mime_type, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, 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 5 PASSED - " "MIME type whitelisted\n"); char mime_whitelist_msg[256]; sprintf(mime_whitelist_msg, "VALIDATOR_DEBUG: RULES ENGINE - MIME whitelist rule matched: %s\n", description ? description : "Unknown"); validator_debug_log(mime_whitelist_msg); sqlite3_finalize(stmt); sqlite3_close(db); return NOSTR_SUCCESS; // Allow whitelisted MIME type } sqlite3_finalize(stmt); } validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 FAILED - MIME " "type not whitelisted\n"); } else { validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 SKIPPED - No " "MIME type provided\n"); } // Step 6: Check if any MIME whitelist rules exist - if yes, deny by default // Match both exact operation and wildcard '*' const char *mime_whitelist_exists_sql = "SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'mime_whitelist' " "AND (operation = ? OR operation = '*') AND enabled = 1 LIMIT 1"; rc = sqlite3_prepare_v2(db, mime_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 mime_whitelist_count = sqlite3_column_int(stmt, 0); if (mime_whitelist_count > 0) { validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 6 FAILED - " "MIME whitelist exists but type not in it\n"); // Set specific violation details for status code mapping strcpy(g_last_rule_violation.violation_type, "mime_whitelist_violation"); strcpy(g_last_rule_violation.reason, "MIME type 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 6 PASSED - No " "MIME whitelist restrictions apply\n"); // Step 7: Check if any whitelist rules exist - if yes, deny by default // Match both exact operation and wildcard '*' const char *whitelist_exists_sql = "SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'pubkey_whitelist' " "AND (operation = ? OR 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 7 PASSED - All " "rule checks completed, default ALLOW\n"); return NOSTR_SUCCESS; // Default allow if no restrictive rules matched } /** * Validate NIP-42 authentication event (kind 22242) */ static int validate_nip42_event(cJSON *event, const char *relay_url, const char *challenge_id) { if (!event || !relay_url || !challenge_id) { return NOSTR_ERROR_INVALID_INPUT; } // Check event kind (must be 22242 for NIP-42) cJSON *kind_json = cJSON_GetObjectItem(event, "kind"); if (!kind_json || !cJSON_IsNumber(kind_json)) { return NOSTR_ERROR_EVENT_INVALID_CONTENT; } int kind = cJSON_GetNumberValue(kind_json); if (kind != NOSTR_NIP42_AUTH_EVENT_KIND) { return NOSTR_ERROR_EVENT_INVALID_CONTENT; } // Validate that the challenge exists and is not expired int challenge_result = validate_challenge(challenge_id); if (challenge_result != NOSTR_SUCCESS) { return challenge_result; } // Use the existing NIP-42 verification from nostr_core_lib int verification_result = nostr_nip42_verify_auth_event(event, challenge_id, relay_url, g_challenge_manager.time_tolerance_seconds); if (verification_result != NOSTR_SUCCESS) { return verification_result; } return NOSTR_SUCCESS; } /** * Validate Admin/Configuration event (kind 33335) */ static int validate_admin_event(cJSON *event, const char *method, const char *endpoint) { if (!event) { return NOSTR_ERROR_INVALID_INPUT; } // Check event kind (must be 33335 for Admin operations) cJSON *kind_json = cJSON_GetObjectItem(event, "kind"); if (!kind_json || !cJSON_IsNumber(kind_json)) { return NOSTR_ERROR_EVENT_INVALID_CONTENT; } int kind = cJSON_GetNumberValue(kind_json); if (kind != 33335) { return NOSTR_ERROR_EVENT_INVALID_KIND; } // Get pubkey for admin authorization check cJSON *pubkey_json = cJSON_GetObjectItem(event, "pubkey"); if (!pubkey_json || !cJSON_IsString(pubkey_json)) { return NOSTR_ERROR_EVENT_INVALID_PUBKEY; } const char *event_pubkey = cJSON_GetStringValue(pubkey_json); if (!event_pubkey || strlen(event_pubkey) != 64) { return NOSTR_ERROR_EVENT_INVALID_PUBKEY; } // Check if pubkey is authorized for admin operations if (strlen(g_auth_cache.admin_pubkey) == 64) { if (strcmp(event_pubkey, g_auth_cache.admin_pubkey) != 0) { validator_debug_log("VALIDATOR_DEBUG: Admin pubkey mismatch - access denied\n"); return NOSTR_ERROR_AUTH_REQUIRED; } } else { validator_debug_log("VALIDATOR_DEBUG: No admin pubkey configured - access denied\n"); return NOSTR_ERROR_AUTH_REQUIRED; } // Validate admin event tag structure cJSON *tags = cJSON_GetObjectItem(event, "tags"); if (!tags || !cJSON_IsArray(tags)) { return NOSTR_ERROR_EVENT_INVALID_CONTENT; } // Track what we find in the event tags int has_method_tag = 0; int has_endpoint_tag = 0; int method_matches = (method == NULL); // If no expected method, consider it matched int endpoint_matches = (endpoint == NULL); // If no expected endpoint, consider it matched time_t expiration = 0; 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, "method") == 0) { has_method_tag = 1; cJSON *method_value = cJSON_GetArrayItem(tag, 1); if (method_value && cJSON_IsString(method_value)) { const char *event_method = cJSON_GetStringValue(method_value); if (method && strcmp(event_method, method) == 0) { method_matches = 1; } } } else if (strcmp(tag_name_str, "endpoint") == 0) { has_endpoint_tag = 1; cJSON *endpoint_value = cJSON_GetArrayItem(tag, 1); if (endpoint_value && cJSON_IsString(endpoint_value)) { const char *event_endpoint = cJSON_GetStringValue(endpoint_value); // For endpoint matching, allow prefix matching for API endpoints if (endpoint && strncmp(event_endpoint, endpoint, strlen(endpoint)) == 0) { endpoint_matches = 1; } } } else if (strcmp(tag_name_str, "expiration") == 0) { cJSON *exp_value = cJSON_GetArrayItem(tag, 1); if (exp_value && cJSON_IsString(exp_value)) { expiration = (time_t)atol(cJSON_GetStringValue(exp_value)); } } } // Admin events should have method and endpoint tags if (!has_method_tag || !has_endpoint_tag) { return NOSTR_ERROR_EVENT_INVALID_CONTENT; } // If we have expected values, they must match if (!method_matches || !endpoint_matches) { return NOSTR_ERROR_EVENT_INVALID_TAGS; } // Check expiration time_t now = time(NULL); if (expiration > 0 && now > expiration) { return NOSTR_ERROR_EVENT_EXPIRED; } validator_debug_log("VALIDATOR_DEBUG: Admin event validation passed\n"); return NOSTR_SUCCESS; } //============================================================================= // 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; } /** * Validate that a challenge exists and is not expired */ static int validate_challenge(const char *challenge_id) { if (!challenge_id || strlen(challenge_id) == 0) { return NOSTR_ERROR_INVALID_INPUT; } cleanup_expired_challenges(); time_t now = time(NULL); for (int i = 0; i < g_challenge_manager.challenge_count; i++) { nip42_challenge_entry_t *entry = &g_challenge_manager.challenges[i]; if (entry->active && strcmp(entry->challenge_id, challenge_id) == 0) { if (now <= entry->expires_at) { char validate_msg[256]; sprintf(validate_msg, "NIP-42: Challenge %.16s... validated successfully\n", challenge_id); validator_debug_log(validate_msg); return NOSTR_SUCCESS; } else { // Mark as expired entry->active = 0; validator_debug_log("NIP-42: Challenge found but expired\n"); return NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED; } } } validator_debug_log("NIP-42: Challenge not found\n"); return NOSTR_ERROR_NIP42_INVALID_CHALLENGE; } /** * 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; }