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