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