/* * NOSTR Core Library - NIP-042: Authentication of clients to relays * * Implements client authentication through signed ephemeral events */ #include "nip042.h" #include "nip001.h" #include "utils.h" #include "../cjson/cJSON.h" #include #include #include #include // Forward declarations for crypto functions int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len); // ============================================================================= // CLIENT-SIDE FUNCTIONS // ============================================================================= /** * Create NIP-42 authentication event (kind 22242) */ cJSON* nostr_nip42_create_auth_event(const char* challenge, const char* relay_url, const unsigned char* private_key, time_t timestamp) { if (!challenge || !relay_url || !private_key) { return NULL; } // Validate challenge format size_t challenge_len = strlen(challenge); if (challenge_len < NOSTR_NIP42_MIN_CHALLENGE_LENGTH || challenge_len >= NOSTR_NIP42_MAX_CHALLENGE_LENGTH) { return NULL; } // Create tags array with relay and challenge cJSON* tags = cJSON_CreateArray(); if (!tags) { return NULL; } // Add relay tag cJSON* relay_tag = cJSON_CreateArray(); if (!relay_tag) { cJSON_Delete(tags); return NULL; } cJSON_AddItemToArray(relay_tag, cJSON_CreateString("relay")); cJSON_AddItemToArray(relay_tag, cJSON_CreateString(relay_url)); cJSON_AddItemToArray(tags, relay_tag); // Add challenge tag cJSON* challenge_tag = cJSON_CreateArray(); if (!challenge_tag) { cJSON_Delete(tags); return NULL; } cJSON_AddItemToArray(challenge_tag, cJSON_CreateString("challenge")); cJSON_AddItemToArray(challenge_tag, cJSON_CreateString(challenge)); cJSON_AddItemToArray(tags, challenge_tag); // Create authentication event using existing function // Note: Empty content as per NIP-42 specification cJSON* auth_event = nostr_create_and_sign_event( NOSTR_NIP42_AUTH_EVENT_KIND, "", // Empty content tags, private_key, timestamp ); cJSON_Delete(tags); return auth_event; } /** * Create AUTH message JSON for relay communication */ char* nostr_nip42_create_auth_message(cJSON* auth_event) { if (!auth_event) { return NULL; } // Create AUTH message array: ["AUTH", ] cJSON* message_array = cJSON_CreateArray(); if (!message_array) { return NULL; } cJSON_AddItemToArray(message_array, cJSON_CreateString("AUTH")); cJSON_AddItemToArray(message_array, cJSON_Duplicate(auth_event, 1)); char* message_string = cJSON_PrintUnformatted(message_array); cJSON_Delete(message_array); return message_string; } /** * Validate challenge string format and freshness */ int nostr_nip42_validate_challenge(const char* challenge, time_t received_at, int time_tolerance) { if (!challenge) { return NOSTR_ERROR_INVALID_INPUT; } size_t challenge_len = strlen(challenge); // Check challenge length if (challenge_len < NOSTR_NIP42_MIN_CHALLENGE_LENGTH) { return NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT; } if (challenge_len >= NOSTR_NIP42_MAX_CHALLENGE_LENGTH) { return NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG; } // Check time validity if provided if (received_at > 0) { time_t now = time(NULL); int tolerance = (time_tolerance > 0) ? time_tolerance : NOSTR_NIP42_DEFAULT_TIME_TOLERANCE; if (now - received_at > tolerance) { return NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED; } } return NOSTR_SUCCESS; } /** * Parse AUTH challenge message from relay */ int nostr_nip42_parse_auth_challenge(const char* message, char* challenge_out, size_t challenge_size) { if (!message || !challenge_out || challenge_size == 0) { return NOSTR_ERROR_INVALID_INPUT; } cJSON* json = cJSON_Parse(message); if (!json || !cJSON_IsArray(json)) { if (json) cJSON_Delete(json); return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT; } // Check array has exactly 2 elements if (cJSON_GetArraySize(json) != 2) { cJSON_Delete(json); return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT; } // Check first element is "AUTH" cJSON* message_type = cJSON_GetArrayItem(json, 0); if (!message_type || !cJSON_IsString(message_type) || strcmp(cJSON_GetStringValue(message_type), "AUTH") != 0) { cJSON_Delete(json); return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT; } // Get challenge string cJSON* challenge_item = cJSON_GetArrayItem(json, 1); if (!challenge_item || !cJSON_IsString(challenge_item)) { cJSON_Delete(json); return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT; } const char* challenge_str = cJSON_GetStringValue(challenge_item); if (!challenge_str || strlen(challenge_str) >= challenge_size) { cJSON_Delete(json); return NOSTR_ERROR_NIP42_INVALID_CHALLENGE; } strcpy(challenge_out, challenge_str); cJSON_Delete(json); return NOSTR_SUCCESS; } // ============================================================================= // SERVER-SIDE FUNCTIONS // ============================================================================= /** * Generate cryptographically secure challenge string */ int nostr_nip42_generate_challenge(char* challenge_out, size_t length) { if (!challenge_out || length < NOSTR_NIP42_MIN_CHALLENGE_LENGTH || length > NOSTR_NIP42_MAX_CHALLENGE_LENGTH / 2) { return NOSTR_ERROR_INVALID_INPUT; } // Generate random bytes unsigned char random_bytes[NOSTR_NIP42_MAX_CHALLENGE_LENGTH / 2]; if (nostr_secp256k1_get_random_bytes(random_bytes, length) != 1) { return NOSTR_ERROR_CRYPTO_FAILED; } // Convert to hex string (reusing existing function) nostr_bytes_to_hex(random_bytes, length, challenge_out); return NOSTR_SUCCESS; } /** * Verify NIP-42 authentication event */ int nostr_nip42_verify_auth_event(cJSON* auth_event, const char* expected_challenge, const char* relay_url, int time_tolerance) { if (!auth_event || !expected_challenge || !relay_url) { return NOSTR_ERROR_INVALID_INPUT; } // First validate basic event structure using existing function int structure_result = nostr_validate_event_structure(auth_event); if (structure_result != NOSTR_SUCCESS) { return structure_result; } // Validate NIP-42 specific structure int nip42_structure_result = nostr_nip42_validate_auth_event_structure( auth_event, relay_url, expected_challenge, time_tolerance); if (nip42_structure_result != NOSTR_SUCCESS) { return nip42_structure_result; } // Finally verify cryptographic signature using existing function return nostr_verify_event_signature(auth_event); } /** * Parse AUTH message from client */ int nostr_nip42_parse_auth_message(const char* message, cJSON** auth_event_out) { if (!message || !auth_event_out) { return NOSTR_ERROR_INVALID_INPUT; } cJSON* json = cJSON_Parse(message); if (!json || !cJSON_IsArray(json)) { if (json) cJSON_Delete(json); return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT; } // Check array has exactly 2 elements if (cJSON_GetArraySize(json) != 2) { cJSON_Delete(json); return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT; } // Check first element is "AUTH" cJSON* message_type = cJSON_GetArrayItem(json, 0); if (!message_type || !cJSON_IsString(message_type) || strcmp(cJSON_GetStringValue(message_type), "AUTH") != 0) { cJSON_Delete(json); return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT; } // Get event object cJSON* event_item = cJSON_GetArrayItem(json, 1); if (!event_item || !cJSON_IsObject(event_item)) { cJSON_Delete(json); return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT; } // Duplicate the event for the caller *auth_event_out = cJSON_Duplicate(event_item, 1); cJSON_Delete(json); if (!*auth_event_out) { return NOSTR_ERROR_MEMORY_FAILED; } return NOSTR_SUCCESS; } /** * Create "auth-required" error response */ char* nostr_nip42_create_auth_required_message(const char* subscription_id, const char* event_id, const char* reason) { const char* default_reason = "authentication required"; const char* message_reason = reason ? reason : default_reason; cJSON* response = cJSON_CreateArray(); if (!response) { return NULL; } if (subscription_id) { // CLOSED message for subscriptions cJSON_AddItemToArray(response, cJSON_CreateString("CLOSED")); cJSON_AddItemToArray(response, cJSON_CreateString(subscription_id)); char prefix_message[512]; snprintf(prefix_message, sizeof(prefix_message), "auth-required: %s", message_reason); cJSON_AddItemToArray(response, cJSON_CreateString(prefix_message)); } else if (event_id) { // OK message for events cJSON_AddItemToArray(response, cJSON_CreateString("OK")); cJSON_AddItemToArray(response, cJSON_CreateString(event_id)); cJSON_AddItemToArray(response, cJSON_CreateBool(0)); // false char prefix_message[512]; snprintf(prefix_message, sizeof(prefix_message), "auth-required: %s", message_reason); cJSON_AddItemToArray(response, cJSON_CreateString(prefix_message)); } else { cJSON_Delete(response); return NULL; } char* message_string = cJSON_PrintUnformatted(response); cJSON_Delete(response); return message_string; } /** * Create "restricted" error response */ char* nostr_nip42_create_restricted_message(const char* subscription_id, const char* event_id, const char* reason) { const char* default_reason = "access restricted"; const char* message_reason = reason ? reason : default_reason; cJSON* response = cJSON_CreateArray(); if (!response) { return NULL; } if (subscription_id) { // CLOSED message for subscriptions cJSON_AddItemToArray(response, cJSON_CreateString("CLOSED")); cJSON_AddItemToArray(response, cJSON_CreateString(subscription_id)); char prefix_message[512]; snprintf(prefix_message, sizeof(prefix_message), "restricted: %s", message_reason); cJSON_AddItemToArray(response, cJSON_CreateString(prefix_message)); } else if (event_id) { // OK message for events cJSON_AddItemToArray(response, cJSON_CreateString("OK")); cJSON_AddItemToArray(response, cJSON_CreateString(event_id)); cJSON_AddItemToArray(response, cJSON_CreateBool(0)); // false char prefix_message[512]; snprintf(prefix_message, sizeof(prefix_message), "restricted: %s", message_reason); cJSON_AddItemToArray(response, cJSON_CreateString(prefix_message)); } else { cJSON_Delete(response); return NULL; } char* message_string = cJSON_PrintUnformatted(response); cJSON_Delete(response); return message_string; } // ============================================================================= // URL NORMALIZATION FUNCTIONS // ============================================================================= /** * Normalize relay URL for comparison */ char* nostr_nip42_normalize_url(const char* url) { if (!url) { return NULL; } size_t url_len = strlen(url); char* normalized = malloc(url_len + 1); if (!normalized) { return NULL; } strcpy(normalized, url); // Remove trailing slash if (url_len > 1 && normalized[url_len - 1] == '/') { normalized[url_len - 1] = '\0'; } // Convert to lowercase for domain comparison for (size_t i = 0; normalized[i]; i++) { if (normalized[i] >= 'A' && normalized[i] <= 'Z') { normalized[i] = normalized[i] + ('a' - 'A'); } } return normalized; } /** * Check if two relay URLs match after normalization */ int nostr_nip42_urls_match(const char* url1, const char* url2) { if (!url1 || !url2) { return -1; } char* norm1 = nostr_nip42_normalize_url(url1); char* norm2 = nostr_nip42_normalize_url(url2); if (!norm1 || !norm2) { free(norm1); free(norm2); return -1; } int result = (strcmp(norm1, norm2) == 0) ? 1 : 0; free(norm1); free(norm2); return result; } // ============================================================================= // UTILITY FUNCTIONS // ============================================================================= /** * Get string description of authentication state */ const char* nostr_nip42_auth_state_str(nostr_auth_state_t state) { switch (state) { case NOSTR_AUTH_STATE_NONE: return "none"; case NOSTR_AUTH_STATE_CHALLENGE_RECEIVED: return "challenge_received"; case NOSTR_AUTH_STATE_AUTHENTICATING: return "authenticating"; case NOSTR_AUTH_STATE_AUTHENTICATED: return "authenticated"; case NOSTR_AUTH_STATE_REJECTED: return "rejected"; default: return "unknown"; } } /** * Initialize authentication context structure */ int nostr_nip42_init_auth_context(nostr_auth_context_t* ctx, const char* relay_url, const char* challenge, int time_tolerance) { if (!ctx || !relay_url || !challenge) { return NOSTR_ERROR_INVALID_INPUT; } memset(ctx, 0, sizeof(nostr_auth_context_t)); ctx->relay_url = malloc(strlen(relay_url) + 1); if (!ctx->relay_url) { return NOSTR_ERROR_MEMORY_FAILED; } strcpy(ctx->relay_url, relay_url); ctx->challenge = malloc(strlen(challenge) + 1); if (!ctx->challenge) { free(ctx->relay_url); ctx->relay_url = NULL; return NOSTR_ERROR_MEMORY_FAILED; } strcpy(ctx->challenge, challenge); ctx->timestamp = time(NULL); ctx->time_tolerance = (time_tolerance > 0) ? time_tolerance : NOSTR_NIP42_DEFAULT_TIME_TOLERANCE; return NOSTR_SUCCESS; } /** * Free authentication context structure */ void nostr_nip42_free_auth_context(nostr_auth_context_t* ctx) { if (!ctx) { return; } free(ctx->relay_url); free(ctx->challenge); free(ctx->pubkey_hex); memset(ctx, 0, sizeof(nostr_auth_context_t)); } /** * Validate authentication event structure (without signature verification) */ int nostr_nip42_validate_auth_event_structure(cJSON* auth_event, const char* relay_url, const char* challenge, int time_tolerance) { if (!auth_event || !relay_url || !challenge) { return NOSTR_ERROR_INVALID_INPUT; } // Check event kind is 22242 cJSON* kind_item = cJSON_GetObjectItem(auth_event, "kind"); if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP42_AUTH_EVENT_KIND) { return NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID; } // Check timestamp is within tolerance cJSON* created_at_item = cJSON_GetObjectItem(auth_event, "created_at"); if (!created_at_item || !cJSON_IsNumber(created_at_item)) { return NOSTR_ERROR_EVENT_INVALID_CREATED_AT; } time_t event_time = (time_t)cJSON_GetNumberValue(created_at_item); time_t now = time(NULL); int tolerance = (time_tolerance > 0) ? time_tolerance : NOSTR_NIP42_DEFAULT_TIME_TOLERANCE; if (abs((int)(now - event_time)) > tolerance) { return NOSTR_ERROR_NIP42_TIME_TOLERANCE; } // Check tags contain required relay and challenge cJSON* tags_item = cJSON_GetObjectItem(auth_event, "tags"); if (!tags_item || !cJSON_IsArray(tags_item)) { return NOSTR_ERROR_EVENT_INVALID_TAGS; } int found_relay = 0, found_challenge = 0; cJSON* tag_item; cJSON_ArrayForEach(tag_item, tags_item) { if (!cJSON_IsArray(tag_item) || cJSON_GetArraySize(tag_item) < 2) { continue; } cJSON* tag_name = cJSON_GetArrayItem(tag_item, 0); cJSON* tag_value = cJSON_GetArrayItem(tag_item, 1); if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) { continue; } const char* name = cJSON_GetStringValue(tag_name); const char* value = cJSON_GetStringValue(tag_value); if (strcmp(name, "relay") == 0) { if (nostr_nip42_urls_match(value, relay_url) == 1) { found_relay = 1; } } else if (strcmp(name, "challenge") == 0) { if (strcmp(value, challenge) == 0) { found_challenge = 1; } } } if (!found_relay) { return NOSTR_ERROR_NIP42_URL_MISMATCH; } if (!found_challenge) { return NOSTR_ERROR_NIP42_INVALID_CHALLENGE; } return NOSTR_SUCCESS; } // ============================================================================= // WEBSOCKET CLIENT INTEGRATION STUB FUNCTIONS // ============================================================================= // Note: These will need to be implemented when WebSocket client structure is available int nostr_ws_authenticate(struct nostr_ws_client* client, const unsigned char* private_key, int time_tolerance) { // TODO: Implement when WebSocket client structure is available (void)client; (void)private_key; (void)time_tolerance; return NOSTR_ERROR_NETWORK_FAILED; // Placeholder } nostr_auth_state_t nostr_ws_get_auth_state(struct nostr_ws_client* client) { // TODO: Implement when WebSocket client structure is available (void)client; return NOSTR_AUTH_STATE_NONE; // Placeholder } int nostr_ws_has_valid_challenge(struct nostr_ws_client* client) { // TODO: Implement when WebSocket client structure is available (void)client; return 0; // Placeholder } int nostr_ws_get_challenge(struct nostr_ws_client* client, char* challenge_out, size_t challenge_size) { // TODO: Implement when WebSocket client structure is available (void)client; (void)challenge_out; (void)challenge_size; return NOSTR_ERROR_NETWORK_FAILED; // Placeholder } int nostr_ws_store_challenge(struct nostr_ws_client* client, const char* challenge) { // TODO: Implement when WebSocket client structure is available (void)client; (void)challenge; return NOSTR_ERROR_NETWORK_FAILED; // Placeholder } int nostr_ws_clear_auth_state(struct nostr_ws_client* client) { // TODO: Implement when WebSocket client structure is available (void)client; return NOSTR_ERROR_NETWORK_FAILED; // Placeholder }