diff --git a/build/admin_api.o b/build/admin_api.o index fec0ba3..eb36873 100644 Binary files a/build/admin_api.o and b/build/admin_api.o differ diff --git a/build/ginxsom-fcgi b/build/ginxsom-fcgi index f78b4bb..668baa2 100755 Binary files a/build/ginxsom-fcgi and b/build/ginxsom-fcgi differ diff --git a/build/main.o b/build/main.o index f48657a..0b1b3d8 100644 Binary files a/build/main.o and b/build/main.o differ diff --git a/build/request_validator.o b/build/request_validator.o index b9c7c2e..d93da8d 100644 Binary files a/build/request_validator.o and b/build/request_validator.o differ diff --git a/db/ginxsom.db b/db/ginxsom.db index 156d05a..11a1706 100644 Binary files a/db/ginxsom.db and b/db/ginxsom.db differ diff --git a/src/admin_api.c b/src/admin_api.c index 41b304a..db83541 100644 --- a/src/admin_api.c +++ b/src/admin_api.c @@ -15,7 +15,7 @@ #define DB_PATH "db/ginxsom.db" // Function declarations (moved from admin_api.h) -void handle_admin_api_request(const char* method, const char* uri); +void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated); void handle_stats_api(void); void handle_config_get_api(void); void handle_config_put_api(void); @@ -120,7 +120,7 @@ static const char* admin_mime_to_extension(const char* mime_type) { } // Main API request handler -void handle_admin_api_request(const char* method, const char* uri) { +void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated) { const char* path = uri + 4; // Skip "/api" // Check if admin interface is enabled @@ -129,15 +129,20 @@ void handle_admin_api_request(const char* method, const char* uri) { return; } - // TODO: Re-enable authentication later - // Authentication temporarily disabled for testing - // if (strcmp(path, "/health") != 0) { - // const char* auth_header = getenv("HTTP_AUTHORIZATION"); - // if (!authenticate_admin_request(auth_header)) { - // send_json_error(401, "admin_auth_required", "Valid admin authentication required"); - // return; - // } - // } + // Authentication now handled by centralized validation system + // Health endpoint is exempt from authentication requirement + if (strcmp(path, "/health") != 0) { + if (!is_authenticated || !validated_pubkey) { + send_json_error(401, "admin_auth_required", "Valid admin authentication required"); + return; + } + + // Verify the authenticated pubkey has admin privileges + if (!verify_admin_pubkey(validated_pubkey)) { + send_json_error(403, "admin_forbidden", "Admin privileges required"); + return; + } + } // Route to appropriate handler if (strcmp(method, "GET") == 0) { diff --git a/src/ginxsom.h b/src/ginxsom.h index d51a8f7..9191df0 100644 --- a/src/ginxsom.h +++ b/src/ginxsom.h @@ -251,7 +251,7 @@ int validate_sha256_format(const char* sha256); ///////////////////////////////////////////////////////////////////////////////////////// // Admin API request handler -void handle_admin_api_request(const char* method, const char* uri); +void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated); // Individual endpoint handlers void handle_stats_api(void); diff --git a/src/main.c b/src/main.c index 4083da9..00b2a70 100644 --- a/src/main.c +++ b/src/main.c @@ -1024,6 +1024,8 @@ void handle_upload_request(void) { fflush(stderr); + // Legacy authentication check - now handled by centralized validation system + /* // Check if authentication rules are enabled using nostr_core_lib system int auth_required = nostr_auth_rules_enabled(); fprintf(stderr, "AUTH: auth_rules_enabled = %d, auth_header present: %s\r\n", @@ -1046,12 +1048,14 @@ void handle_upload_request(void) { // Skip validation and proceed to file processing goto process_file_upload; } + */ // Authentication is handled by centralized validation system // TODO: Get uploader_pubkey from centralized validation result // For now, keep existing uploader_pubkey extraction for compatibility -process_file_upload: +// Legacy goto label - no longer needed with centralized validation +// process_file_upload: // Get dimensions from in-memory buffer before saving file int width = 0, height = 0; nip94_get_dimensions(file_data, content_length, content_type, &width, @@ -1295,6 +1299,8 @@ void handle_upload_request_with_validation(nostr_request_result_t* validation_re fflush(stderr); + // Legacy authentication check - now handled by centralized validation system + /* // Check if authentication rules are enabled using nostr_core_lib system int auth_required = nostr_auth_rules_enabled(); fprintf(stderr, "AUTH: auth_rules_enabled = %d, auth_header present: %s\r\n", @@ -1317,11 +1323,13 @@ void handle_upload_request_with_validation(nostr_request_result_t* validation_re // Skip validation and proceed to file processing goto process_file_upload; } + */ // Authentication was handled by centralized validation system // uploader_pubkey should be set from validation result -process_file_upload: +// Legacy goto label - no longer needed with centralized validation +// process_file_upload: // Get dimensions from in-memory buffer before saving file int width = 0, height = 0; nip94_get_dimensions(file_data, file_size, content_type, &width, @@ -1650,6 +1658,8 @@ if (!config_loaded /* && !initialize_server_config() */) { // HEAD requests might not require auth depending on config - let handler decide } else if (strcmp(operation, "list") == 0) { // List operation might be optional auth - let handler decide + } else if (strcmp(operation, "admin") == 0 && strcmp(request_uri, "/api/health") == 0) { + // Health endpoint is public and doesn't require authentication - let handler decide } else { // For other operations, validation failure means auth failure const char *message = result.reason[0] ? result.reason : "Authentication failed"; @@ -1732,8 +1742,8 @@ if (!config_loaded /* && !initialize_server_config() */) { } else if (strncmp(request_uri, "/api/", 5) == 0) { // Handle admin API requests with pre-validated auth - // TODO: Pass validated result to existing handler - handle_admin_api_request(request_method, request_uri); + const char *validated_pubkey = (result.valid && strlen(result.pubkey) == 64) ? result.pubkey : NULL; + handle_admin_api_request(request_method, request_uri, validated_pubkey, result.valid); } else if (strcmp(request_method, "GET") == 0 && diff --git a/src/request_validator.c b/src/request_validator.c index 22f09b8..a539952 100644 --- a/src/request_validator.c +++ b/src/request_validator.c @@ -113,6 +113,7 @@ 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); void nostr_request_validator_clear_violation(void); @@ -659,6 +660,79 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request, "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, @@ -668,7 +742,7 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request, 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 or kind 24242 for Blossom.", + "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; @@ -1423,6 +1497,118 @@ static int validate_nip42_event(cJSON *event, const char *relay_url, 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 //============================================================================= diff --git a/tests/admin_test.sh b/tests/admin_test.sh index ba5215c..b483bd4 100755 --- a/tests/admin_test.sh +++ b/tests/admin_test.sh @@ -83,12 +83,14 @@ generate_admin_keys() { create_admin_event() { local method="$1" + local endpoint="$2" local content="admin_request" local expiration=$(($(date +%s) + 3600)) # 1 hour from now - # Create Nostr event with nak - always use "admin" tag for admin operations - local event=$(nak event -k 24242 -c "$content" \ - --tag t="admin" \ + # Create Nostr event with nak - use kind 33335 for admin/configuration events + local event=$(nak event -k 33335 -c "$content" \ + --tag method="$method" \ + --tag endpoint="$endpoint" \ --tag expiration="$expiration" \ --sec "$ADMIN_PRIVKEY") @@ -102,8 +104,8 @@ send_admin_request() { log_info "Testing $method $endpoint" - # Create authenticated Nostr event - local event=$(create_admin_event "$method") + # Create authenticated Nostr event with method and endpoint + local event=$(create_admin_event "$method" "$endpoint") local auth_header="Nostr $(echo "$event" | base64 -w 0)" # Send request with curl