Files
c-relay/src/request_validator.c

909 lines
31 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 "debug.h" // C-relay debug system
#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 - already defined in nostr_common.h)
// Forward declarations for NIP-42 functions (simple implementations for C-relay)
int nostr_nip42_generate_challenge(char *challenge_buffer, size_t buffer_size);
int nostr_nip42_verify_auth_event(cJSON *event, const char *challenge_id,
const char *relay_url, int time_tolerance_seconds);
// Type definitions for unified request validation
typedef struct {
const char *operation; // HTTP method or operation type
const char *auth_header; // Authorization header value
const char *client_ip; // Client IP address
const char *request_url; // Full request URL
const char *resource_hash; // Resource hash for validation
const char *challenge_id; // NIP-42 challenge ID
long file_size; // File size for upload validation
int nip42_enabled; // Whether NIP-42 is enabled
} nostr_unified_request_t;
typedef struct {
int valid; // Whether request is valid
int error_code; // Error code if invalid
char reason[512]; // Reason for validation result
char pubkey[65]; // Extracted pubkey from event
unsigned char *file_data; // File data for uploads
size_t file_size; // Size of file data
int owns_file_data; // Whether we own the file data memory
char expected_hash[65]; // Expected file hash
} nostr_request_result_t;
// Additional error codes for ginxsom-specific functionality
#define NOSTR_ERROR_CRYPTO_INIT -100
#define NOSTR_ERROR_AUTH_REQUIRED -101
#define NOSTR_ERROR_NIP42_DISABLED -102
#define NOSTR_ERROR_EVENT_EXPIRED -103
// Note: NOSTR_ERROR_NIP42_CHALLENGE_NOT_FOUND and
// NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED are already defined in
// nostr_core_lib/nostr_core/nostr_common.h
// Note: Using g_database_path from C-relay global state instead of hardcoded path
// NIP-42 challenge management constants
#define MAX_CHALLENGES 1000
#define CHALLENGE_CLEANUP_INTERVAL 300 // 5 minutes
//=============================================================================
// DATA STRUCTURES
//=============================================================================
// NIP-42 challenge storage
typedef struct {
char challenge_id[65];
char client_ip[64];
time_t created_at;
time_t expires_at;
int active;
} nip42_challenge_entry_t;
// NIP-42 challenge management
typedef struct {
nip42_challenge_entry_t challenges[MAX_CHALLENGES];
int challenge_count;
time_t last_cleanup;
int timeout_seconds;
int time_tolerance_seconds;
} nip42_challenge_manager_t;
//=============================================================================
// GLOBAL STATE
//=============================================================================
// No longer using local auth cache - using unified cache from config.c
static nip42_challenge_manager_t g_challenge_manager = {0};
static int g_validator_initialized = 0;
// Last rule violation details for status code mapping
struct {
char violation_type[100]; // "pubkey_blacklist", "hash_blacklist",
// "whitelist_violation", etc.
char reason[500]; // specific reason string
} g_last_rule_violation = {0};
//=============================================================================
// FORWARD DECLARATIONS
//=============================================================================
static int reload_auth_config(void);
// Removed unused forward declarations for functions that are no longer called
int check_database_auth_rules(const char *pubkey, const char *operation,
const char *resource_hash);
void nostr_request_validator_clear_violation(void);
// NIP-42 challenge management functions
static void cleanup_expired_challenges(void);
static int store_challenge(const char *challenge_id, const char *client_ip);
static int generate_challenge_id(char *challenge_buffer, size_t buffer_size);
//=============================================================================
// MAIN API FUNCTIONS
//=============================================================================
/**
* Initialize the ginxsom request validator system
*/
int ginxsom_request_validator_init(const char *db_path, const char *app_name) {
// Mark db_path as unused to suppress warning - it's for future use
(void)db_path;
(void)app_name;
if (g_validator_initialized) {
return NOSTR_SUCCESS; // Already initialized
}
// Initialize nostr_core_lib if not already done
if (nostr_crypto_init() != NOSTR_SUCCESS) {
return NOSTR_ERROR_CRYPTO_INIT;
}
// Load initial configuration from database
int result = reload_auth_config();
if (result != NOSTR_SUCCESS) {
return result;
}
// Initialize NIP-42 challenge manager using unified config
memset(&g_challenge_manager, 0, sizeof(g_challenge_manager));
const char* nip42_timeout = get_config_value("nip42_challenge_timeout");
g_challenge_manager.timeout_seconds = nip42_timeout ? atoi(nip42_timeout) : 600;
if (nip42_timeout) free((char*)nip42_timeout);
const char* nip42_tolerance = get_config_value("nip42_time_tolerance");
g_challenge_manager.time_tolerance_seconds = nip42_tolerance ? atoi(nip42_tolerance) : 300;
if (nip42_tolerance) free((char*)nip42_tolerance);
g_challenge_manager.last_cleanup = time(NULL);
g_validator_initialized = 1;
return NOSTR_SUCCESS;
}
/**
* Check if authentication rules are enabled
*/
int nostr_auth_rules_enabled(void) {
// Use unified cache from config.c
const char* auth_enabled = get_config_value("auth_enabled");
int result = 0;
if (auth_enabled && strcmp(auth_enabled, "true") == 0) {
result = 1;
}
if (auth_enabled) free((char*)auth_enabled);
// Also check legacy key
const char* auth_rules_enabled = get_config_value("auth_rules_enabled");
if (auth_rules_enabled && strcmp(auth_rules_enabled, "true") == 0) {
result = 1;
}
if (auth_rules_enabled) free((char*)auth_rules_enabled);
return result;
}
///////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////
// MAIN VALIDATION OF REQUEST
///////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////
int nostr_validate_unified_request(const char* json_string, size_t json_length) {
// Clear previous violation details
nostr_request_validator_clear_violation();
/////////////////////////////////////////////////////////////////////
// PHASE 1: INPUT VALIDATION (Immediate Rejection ~1μs)
/////////////////////////////////////////////////////////////////////
// 1. Null Pointer Checks - Reject malformed requests instantly
if (!json_string || json_length == 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
// 2. Initialization Check - Verify system is properly initialized
if (!g_validator_initialized) {
return NOSTR_ERROR_INVALID_INPUT;
}
// 3. Parse JSON string to cJSON event object
cJSON *event = cJSON_ParseWithLength(json_string, json_length);
if (!event) {
return NOSTR_ERROR_INVALID_INPUT;
}
// 4. Validate basic event structure
cJSON *id = cJSON_GetObjectItem(event, "id");
cJSON *pubkey = cJSON_GetObjectItem(event, "pubkey");
cJSON *created_at = cJSON_GetObjectItem(event, "created_at");
cJSON *kind = cJSON_GetObjectItem(event, "kind");
cJSON *tags = cJSON_GetObjectItem(event, "tags");
cJSON *content = cJSON_GetObjectItem(event, "content");
cJSON *sig = cJSON_GetObjectItem(event, "sig");
if (!id || !cJSON_IsString(id) ||
!pubkey || !cJSON_IsString(pubkey) ||
!created_at || !cJSON_IsNumber(created_at) ||
!kind || !cJSON_IsNumber(kind) ||
!tags || !cJSON_IsArray(tags) ||
!content || !cJSON_IsString(content) ||
!sig || !cJSON_IsString(sig)) {
cJSON_Delete(event);
return NOSTR_ERROR_INVALID_INPUT;
}
int event_kind = (int)cJSON_GetNumberValue(kind);
// 5. Check configuration using unified cache
int auth_required = nostr_auth_rules_enabled();
/////////////////////////////////////////////////////////////////////
// PHASE 2: NOSTR EVENT VALIDATION
/////////////////////////////////////////////////////////////////////
// 6. Nostr Event Structure Validation using nostr_core_lib
int validation_result = nostr_validate_event(event);
if (validation_result != NOSTR_SUCCESS) {
cJSON_Delete(event);
return validation_result;
}
// 7. Extract pubkey for rule evaluation
const char *event_pubkey = cJSON_GetStringValue(pubkey);
if (!event_pubkey || strlen(event_pubkey) != 64) {
cJSON_Delete(event);
return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
}
/////////////////////////////////////////////////////////////////////
// PHASE 3: ADMIN EVENT BYPASS CHECK
/////////////////////////////////////////////////////////////////////
// 8. Check if this is a kind 23456 admin event from authorized admin
// This must happen AFTER signature validation but BEFORE auth rules
if (event_kind == 23456) {
const char* admin_pubkey = get_config_value("admin_pubkey");
if (admin_pubkey && strcmp(event_pubkey, admin_pubkey) == 0) {
// Valid admin event - bypass remaining validation
cJSON_Delete(event);
return NOSTR_SUCCESS;
}
// Not from admin - continue with normal validation
}
/////////////////////////////////////////////////////////////////////
// PHASE 4: EVENT KIND SPECIFIC VALIDATION
/////////////////////////////////////////////////////////////////////
// 9. Handle NIP-42 authentication challenge events (kind 22242)
if (event_kind == 22242) {
// Check NIP-42 mode using unified cache
const char* nip42_enabled = get_config_value("nip42_auth_enabled");
if (nip42_enabled && strcmp(nip42_enabled, "false") == 0) {
free((char*)nip42_enabled);
cJSON_Delete(event);
return NOSTR_ERROR_NIP42_DISABLED;
}
if (nip42_enabled) free((char*)nip42_enabled);
// TODO: Implement full NIP-42 challenge validation
// For now, accept all valid NIP-42 events
cJSON_Delete(event);
return NOSTR_SUCCESS;
}
/////////////////////////////////////////////////////////////////////
// PHASE 5: AUTHENTICATION RULES (Database Queries)
/////////////////////////////////////////////////////////////////////
// 10. Check if authentication rules are enabled
if (!auth_required) {
} else {
// 11. Check database authentication rules (only if auth enabled)
// Create operation string with event kind for more specific rule matching
char operation_str[64];
snprintf(operation_str, sizeof(operation_str), "event_kind_%d", event_kind);
// Also check generic "event" operation for backward compatibility
int rules_result = check_database_auth_rules(event_pubkey, "event", NULL);
if (rules_result != NOSTR_SUCCESS) {
// If generic event check fails, try specific event kind check
rules_result = check_database_auth_rules(event_pubkey, operation_str, NULL);
if (rules_result != NOSTR_SUCCESS) {
cJSON_Delete(event);
return rules_result;
}
}
}
/////////////////////////////////////////////////////////////////////
// PHASE 6: ADDITIONAL VALIDATIONS (C-relay specific)
/////////////////////////////////////////////////////////////////////
// 12. NIP-13 Proof of Work validation
int pow_enabled = get_config_bool("pow_enabled", 0);
int pow_min_difficulty = get_config_int("pow_min_difficulty", 0);
int pow_validation_flags = get_config_int("pow_validation_flags", 1);
if (pow_enabled && pow_min_difficulty > 0) {
nostr_pow_result_t pow_result;
int pow_validation_result = nostr_validate_pow(event, pow_min_difficulty,
pow_validation_flags, &pow_result);
if (pow_validation_result != NOSTR_SUCCESS) {
cJSON_Delete(event);
return pow_validation_result;
}
}
// 13. NIP-40 Expiration validation
// Always check expiration tags if present (following NIP-40 specification)
cJSON *expiration_tag = NULL;
cJSON *tags_array = cJSON_GetObjectItem(event, "tags");
if (tags_array && cJSON_IsArray(tags_array)) {
cJSON *tag = NULL;
cJSON_ArrayForEach(tag, tags_array) {
if (!cJSON_IsArray(tag)) continue;
cJSON *tag_name = cJSON_GetArrayItem(tag, 0);
if (!tag_name || !cJSON_IsString(tag_name)) continue;
const char *tag_name_str = cJSON_GetStringValue(tag_name);
if (strcmp(tag_name_str, "expiration") == 0) {
cJSON *tag_value = cJSON_GetArrayItem(tag, 1);
if (tag_value && cJSON_IsString(tag_value)) {
expiration_tag = tag_value;
break;
}
}
}
}
if (expiration_tag) {
const char *expiration_str = cJSON_GetStringValue(expiration_tag);
// Validate that the expiration string contains only digits (and optional leading whitespace)
const char* p = expiration_str;
// Skip leading whitespace
while (*p == ' ' || *p == '\t') p++;
// Check if we have at least one digit
if (*p == '\0') {
} else {
// Validate that all remaining characters are digits
const char* digit_start = p;
while (*p >= '0' && *p <= '9') p++;
// If we didn't consume the entire string or found no digits, it's malformed
if (*p != '\0' || p == digit_start) {
} else {
// Valid numeric string, parse and check expiration
time_t expiration_time = (time_t)atol(expiration_str);
time_t now = time(NULL);
int grace_period = get_config_int("nip40_expiration_grace_period", 60);
if (expiration_time > 0 && now > expiration_time + grace_period) {
cJSON_Delete(event);
return NOSTR_ERROR_EVENT_EXPIRED;
}
}
}
}
// All validations passed
cJSON_Delete(event);
return NOSTR_SUCCESS;
}
/**
* Generate NIP-42 challenge for clients
*/
int nostr_request_validator_generate_nip42_challenge(void *challenge_struct,
const char *client_ip) {
// Mark client_ip as unused to suppress warning - it's for future enhancement
(void)client_ip;
// Use nostr_core_lib NIP-42 functionality
char challenge_id[65];
int result = nostr_nip42_generate_challenge(challenge_id, 32);
if (result != NOSTR_SUCCESS) {
return result;
}
// Fill challenge structure (assuming it's a compatible structure)
// This is a simplified implementation - adjust based on actual structure
// needs
if (challenge_struct) {
// Cast to appropriate structure and fill fields
// For now, just return success
}
return NOSTR_SUCCESS;
}
/**
* Get the last rule violation type for status code mapping
*/
const char *nostr_request_validator_get_last_violation_type(void) {
return g_last_rule_violation.violation_type;
}
/**
* Clear the last rule violation details
*/
void nostr_request_validator_clear_violation(void) {
memset(&g_last_rule_violation, 0, sizeof(g_last_rule_violation));
}
/**
* Cleanup request validator resources
*/
void ginxsom_request_validator_cleanup(void) {
g_validator_initialized = 0;
nostr_request_validator_clear_violation();
}
/**
* Free file data allocated by validator
*/
void nostr_request_result_free_file_data(nostr_request_result_t *result) {
if (result && result->file_data && result->owns_file_data) {
free(result->file_data);
result->file_data = NULL;
result->file_size = 0;
result->owns_file_data = 0;
}
}
//=============================================================================
// HELPER FUNCTIONS
//=============================================================================
/**
* Force cache refresh - cache no longer exists, function kept for compatibility
*/
void nostr_request_validator_force_cache_refresh(void) {
// Cache no longer exists - direct database queries are used
}
/**
* This function is no longer needed - configuration is handled by unified cache
*/
static int reload_auth_config(void) {
// Configuration is now handled by the unified cache in config.c
return NOSTR_SUCCESS;
}
// Removed unused functions: parse_authorization_header, extract_pubkey_from_event
// Note: Blossom protocol validation removed - C-relay uses standard Nostr events only
/**
* Check database authentication rules for the request
* Implements the 6-step rule evaluation engine from AUTH_API.md
*/
int check_database_auth_rules(const char *pubkey, const char *operation __attribute__((unused)),
const char *resource_hash) {
sqlite3 *db = NULL;
sqlite3_stmt *stmt = NULL;
int rc;
DEBUG_TRACE("Checking auth rules for pubkey: %s", pubkey);
if (!pubkey) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Open database using global database path
if (strlen(g_database_path) == 0) {
return NOSTR_SUCCESS; // Default allow on DB error
}
rc = sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
return NOSTR_SUCCESS; // Default allow on DB error
}
// Step 1: Check pubkey blacklist (highest priority)
const char *blacklist_sql =
"SELECT rule_type FROM auth_rules WHERE rule_type = "
"'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1";
DEBUG_TRACE("Blacklist SQL: %s", blacklist_sql);
rc = sqlite3_prepare_v2(db, blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
int step_result = sqlite3_step(stmt);
DEBUG_TRACE("Blacklist query result: %s", step_result == SQLITE_ROW ? "FOUND" : "NOT_FOUND");
if (step_result == SQLITE_ROW) {
DEBUG_TRACE("BLACKLIST HIT: Denying access for pubkey: %s", pubkey);
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "pubkey_blacklist");
sprintf(g_last_rule_violation.reason, "Public key blacklisted");
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_ERROR_AUTH_REQUIRED;
}
sqlite3_finalize(stmt);
}
// Step 2: Check hash blacklist
if (resource_hash) {
const char *hash_blacklist_sql =
"SELECT rule_type FROM auth_rules WHERE rule_type = "
"'blacklist' AND pattern_type = 'hash' AND pattern_value = ? AND active = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, hash_blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, resource_hash, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "hash_blacklist");
sprintf(g_last_rule_violation.reason, "File hash blacklisted");
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_ERROR_AUTH_REQUIRED;
}
sqlite3_finalize(stmt);
}
}
// Step 3: Check pubkey whitelist
const char *whitelist_sql =
"SELECT rule_type FROM auth_rules WHERE rule_type = "
"'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, whitelist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_SUCCESS; // Allow whitelisted pubkey
}
sqlite3_finalize(stmt);
}
// Step 4: Check if any whitelist rules exist - if yes, deny by default
const char *whitelist_exists_sql =
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'whitelist' "
"AND pattern_type = 'pubkey' AND active = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, whitelist_exists_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
int whitelist_count = sqlite3_column_int(stmt, 0);
if (whitelist_count > 0) {
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "whitelist_violation");
strcpy(g_last_rule_violation.reason,
"Public key not whitelisted for this operation");
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_ERROR_AUTH_REQUIRED;
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
return NOSTR_SUCCESS; // Default allow if no restrictive rules matched
}
// Removed unused functions: validate_nip42_event, validate_admin_event
//=============================================================================
// NIP-42 CHALLENGE MANAGEMENT FUNCTIONS
//=============================================================================
/**
* Generate a challenge ID using nostr_core_lib
*/
static int generate_challenge_id(char *challenge_buffer, size_t buffer_size) {
if (!challenge_buffer || buffer_size < 65) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Use nostr_core_lib to generate a random challenge
return nostr_nip42_generate_challenge(challenge_buffer, 32);
}
/**
* Clean up expired challenges from memory
*/
static void cleanup_expired_challenges(void) {
time_t now = time(NULL);
// Only cleanup if enough time has passed
if (now - g_challenge_manager.last_cleanup < CHALLENGE_CLEANUP_INTERVAL) {
return;
}
int active_count = 0;
for (int i = 0; i < g_challenge_manager.challenge_count; i++) {
if (g_challenge_manager.challenges[i].active) {
if (now > g_challenge_manager.challenges[i].expires_at) {
// Mark expired challenge as inactive
g_challenge_manager.challenges[i].active = 0;
memset(g_challenge_manager.challenges[i].challenge_id, 0,
sizeof(g_challenge_manager.challenges[i].challenge_id));
} else {
active_count++;
}
}
}
// Compact the array if we have many inactive entries
if (active_count < g_challenge_manager.challenge_count / 2 &&
active_count < MAX_CHALLENGES - 100) {
int write_idx = 0;
for (int read_idx = 0; read_idx < g_challenge_manager.challenge_count;
read_idx++) {
if (g_challenge_manager.challenges[read_idx].active) {
if (write_idx != read_idx) {
memcpy(&g_challenge_manager.challenges[write_idx],
&g_challenge_manager.challenges[read_idx],
sizeof(nip42_challenge_entry_t));
}
write_idx++;
}
}
g_challenge_manager.challenge_count = write_idx;
}
g_challenge_manager.last_cleanup = now;
}
/**
* Store a new challenge in memory
*/
static int store_challenge(const char *challenge_id, const char *client_ip) {
if (!challenge_id || strlen(challenge_id) == 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
cleanup_expired_challenges();
// Find an available slot
int slot_idx = -1;
// First, try to find an inactive slot
for (int i = 0; i < g_challenge_manager.challenge_count; i++) {
if (!g_challenge_manager.challenges[i].active) {
slot_idx = i;
break;
}
}
// If no inactive slot found, use next available if we haven't hit max
if (slot_idx == -1 && g_challenge_manager.challenge_count < MAX_CHALLENGES) {
slot_idx = g_challenge_manager.challenge_count++;
}
// If still no slot, we're full - remove oldest entry
if (slot_idx == -1) {
slot_idx = 0; // Overwrite first entry (oldest)
}
// Store the new challenge
nip42_challenge_entry_t *entry = &g_challenge_manager.challenges[slot_idx];
memset(entry, 0, sizeof(nip42_challenge_entry_t));
// Store challenge with proper length handling (up to buffer size - 1)
strncpy(entry->challenge_id, challenge_id, sizeof(entry->challenge_id) - 1);
entry->challenge_id[sizeof(entry->challenge_id) - 1] = '\0';
if (client_ip) {
strncpy(entry->client_ip, client_ip, sizeof(entry->client_ip) - 1);
entry->client_ip[sizeof(entry->client_ip) - 1] = '\0';
}
time_t now = time(NULL);
entry->created_at = now;
entry->expires_at = now + g_challenge_manager.timeout_seconds;
entry->active = 1;
return NOSTR_SUCCESS;
}
/**
* Generate and store a new NIP-42 challenge for /auth endpoint
*/
int nostr_generate_nip42_challenge(char *challenge_out, size_t challenge_size,
const char *client_ip) {
if (!challenge_out || challenge_size < 65) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Generate challenge ID
int result = generate_challenge_id(challenge_out, challenge_size);
if (result != NOSTR_SUCCESS) {
return result;
}
// Store in challenge manager
result = store_challenge(challenge_out, client_ip);
if (result != NOSTR_SUCCESS) {
return result;
}
return NOSTR_SUCCESS;
}
//=============================================================================
// SIMPLE NIP-42 FUNCTION IMPLEMENTATIONS FOR C-RELAY
//=============================================================================
/**
* Simple NIP-42 challenge generation (generates random hex string)
*/
int nostr_nip42_generate_challenge(char *challenge_buffer, size_t buffer_size) {
if (!challenge_buffer || buffer_size < 65) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Generate 32 random bytes and convert to hex string
unsigned char random_bytes[32];
// Simple random number generation using time and rand()
srand((unsigned int)time(NULL));
for (int i = 0; i < 32; i++) {
random_bytes[i] = (unsigned char)(rand() % 256);
}
// Convert to hex string
for (int i = 0; i < 32; i++) {
sprintf(&challenge_buffer[i * 2], "%02x", random_bytes[i]);
}
challenge_buffer[64] = '\0';
return NOSTR_SUCCESS;
}
/**
* Simple NIP-42 auth event verification (validates basic structure and expiration)
*/
int nostr_nip42_verify_auth_event(cJSON *event, const char *challenge_id,
const char *relay_url, int time_tolerance_seconds) {
if (!event || !challenge_id || !relay_url) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Check if event has the required tags for NIP-42
cJSON *tags = cJSON_GetObjectItem(event, "tags");
if (!tags || !cJSON_IsArray(tags)) {
return NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID;
}
int has_relay_tag = 0;
int has_challenge_tag = 0;
time_t expiration = 0;
// Look for required tags
cJSON *tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag)) continue;
cJSON *tag_name = cJSON_GetArrayItem(tag, 0);
if (!tag_name || !cJSON_IsString(tag_name)) continue;
const char *tag_name_str = cJSON_GetStringValue(tag_name);
if (strcmp(tag_name_str, "relay") == 0) {
has_relay_tag = 1;
cJSON *relay_value = cJSON_GetArrayItem(tag, 1);
if (relay_value && cJSON_IsString(relay_value)) {
const char *event_relay = cJSON_GetStringValue(relay_value);
// For C-relay, accept the relay URL as valid (basic check)
if (event_relay && strlen(event_relay) > 0) {
// Relay URL validation passed
}
}
} else if (strcmp(tag_name_str, "challenge") == 0) {
has_challenge_tag = 1;
cJSON *challenge_value = cJSON_GetArrayItem(tag, 1);
if (challenge_value && cJSON_IsString(challenge_value)) {
const char *event_challenge = cJSON_GetStringValue(challenge_value);
if (!event_challenge || strcmp(event_challenge, challenge_id) != 0) {
return NOSTR_ERROR_NIP42_INVALID_CHALLENGE;
}
}
} else if (strcmp(tag_name_str, "expiration") == 0) {
cJSON *exp_value = cJSON_GetArrayItem(tag, 1);
if (exp_value && cJSON_IsString(exp_value)) {
const char *exp_str = cJSON_GetStringValue(exp_value);
// Validate that the expiration string contains only digits (and optional leading whitespace)
const char* p = exp_str;
// Skip leading whitespace
while (*p == ' ' || *p == '\t') p++;
// Check if we have at least one digit and all remaining characters are digits
if (*p != '\0') {
const char* digit_start = p;
while (*p >= '0' && *p <= '9') p++;
// If we consumed the entire string and found at least one digit, it's valid
if (*p == '\0' && p > digit_start) {
expiration = (time_t)atol(exp_str);
}
// If malformed, expiration remains 0 (no expiration), effectively ignoring the tag
}
}
}
}
// Check required tags are present
if (!has_relay_tag || !has_challenge_tag) {
return NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID;
}
// Check expiration with time tolerance
time_t now = time(NULL);
if (expiration > 0) {
if (now > expiration + time_tolerance_seconds) {
return NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED;
}
}
// Check created_at timestamp for reasonable bounds
cJSON *created_at_json = cJSON_GetObjectItem(event, "created_at");
if (created_at_json && cJSON_IsNumber(created_at_json)) {
time_t created_at = (time_t)cJSON_GetNumberValue(created_at_json);
if (abs((int)(now - created_at)) > time_tolerance_seconds) {
return NOSTR_ERROR_NIP42_TIME_TOLERANCE;
}
}
return NOSTR_SUCCESS;
}