diff --git a/build.sh b/build.sh index e40fc48a..6b2b7c36 100755 --- a/build.sh +++ b/build.sh @@ -135,6 +135,7 @@ if [ "$HELP" = true ]; then echo " 011 - Relay information document" echo " 013 - Proof of Work" echo " 019 - Bech32 encoding (nsec/npub)" + echo " 042 - Authentication of clients to relays" echo " 044 - Encryption (modern)" echo "" echo "Examples:" @@ -184,7 +185,7 @@ print_info "Auto-detecting needed NIPs from your source code..." NEEDED_NIPS="" if [ -n "$FORCE_NIPS" ]; then if [ "$FORCE_NIPS" = "all" ]; then - NEEDED_NIPS="001 004 005 006 011 013 019 044" + NEEDED_NIPS="001 004 005 006 011 013 019 042 044" print_info "Forced: Building all available NIPs" else # Convert comma-separated list to space-separated with 3-digit format @@ -203,7 +204,7 @@ else # Check for nostr_core.h (includes everything) if grep -q '#include[[:space:]]*["\<]nostr_core\.h["\>]' *.c 2>/dev/null; then print_info "Found #include \"nostr_core.h\" - building all NIPs" - NEEDED_NIPS="001 004 005 006 011 013 019 044" + NEEDED_NIPS="001 004 005 006 011 013 019 042 044" elif [ -n "$DETECTED" ]; then NEEDED_NIPS="$DETECTED" print_success "Auto-detected NIPs: $(echo $NEEDED_NIPS | tr ' ' ',')" @@ -221,7 +222,7 @@ fi # If building tests, include all NIPs to ensure test compatibility if [ "$BUILD_TESTS" = true ] && [ -z "$FORCE_NIPS" ]; then - NEEDED_NIPS="001 004 005 006 011 013 019 044" + NEEDED_NIPS="001 004 005 006 011 013 019 042 044" print_info "Building tests - including all available NIPs for test compatibility" fi @@ -506,6 +507,7 @@ for nip in $NEEDED_NIPS; do 011) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-011(Relay-Info)" ;; 013) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-013(PoW)" ;; 019) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-019(Bech32)" ;; + 042) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-042(Auth)" ;; 044) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-044(Encrypt)" ;; esac else diff --git a/nostr_core/nip042.c b/nostr_core/nip042.c new file mode 100644 index 00000000..60398c44 --- /dev/null +++ b/nostr_core/nip042.c @@ -0,0 +1,628 @@ +/* + * 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 +} \ No newline at end of file diff --git a/nostr_core/nip042.h b/nostr_core/nip042.h new file mode 100644 index 00000000..b565920d --- /dev/null +++ b/nostr_core/nip042.h @@ -0,0 +1,281 @@ +/* + * NOSTR Core Library - NIP-042: Authentication of clients to relays + * + * Implements client authentication through signed ephemeral events + */ + +#ifndef NIP042_H +#define NIP042_H + +#include +#include +#include +#include "../cjson/cJSON.h" +#include "nostr_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// ============================================================================= +// NIP-42 CONSTANTS AND DEFINITIONS +// ============================================================================= + +#define NOSTR_NIP42_AUTH_EVENT_KIND 22242 +#define NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH 32 +#define NOSTR_NIP42_DEFAULT_TIME_TOLERANCE 600 // 10 minutes in seconds +#define NOSTR_NIP42_MAX_CHALLENGE_LENGTH 256 +#define NOSTR_NIP42_MIN_CHALLENGE_LENGTH 16 + +// Authentication states for WebSocket client integration +typedef enum { + NOSTR_AUTH_STATE_NONE = 0, // No authentication attempted + NOSTR_AUTH_STATE_CHALLENGE_RECEIVED = 1, // Challenge received from relay + NOSTR_AUTH_STATE_AUTHENTICATING = 2, // AUTH event sent, waiting for OK + NOSTR_AUTH_STATE_AUTHENTICATED = 3, // Successfully authenticated + NOSTR_AUTH_STATE_REJECTED = 4 // Authentication rejected +} nostr_auth_state_t; + +// Challenge storage structure +typedef struct { + char challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH]; + time_t received_at; + int is_valid; +} nostr_auth_challenge_t; + +// Authentication context for relay verification +typedef struct { + char* relay_url; + char* challenge; + time_t timestamp; + int time_tolerance; + char* pubkey_hex; +} nostr_auth_context_t; + +// ============================================================================= +// CLIENT-SIDE FUNCTIONS (for nostr clients) +// ============================================================================= + +/** + * Create NIP-42 authentication event (kind 22242) + * @param challenge Challenge string received from relay + * @param relay_url Relay URL (normalized) + * @param private_key 32-byte private key for signing + * @param timestamp Event timestamp (0 for current time) + * @return cJSON event object or NULL on error + */ +cJSON* nostr_nip42_create_auth_event(const char* challenge, + const char* relay_url, + const unsigned char* private_key, + time_t timestamp); + +/** + * Create AUTH message JSON for relay communication + * @param auth_event Authentication event (kind 22242) + * @return JSON string for AUTH message or NULL on error (caller must free) + */ +char* nostr_nip42_create_auth_message(cJSON* auth_event); + +/** + * Validate challenge string format and freshness + * @param challenge Challenge string to validate + * @param received_at Time when challenge was received (0 for no time check) + * @param time_tolerance Maximum age in seconds (0 for default) + * @return NOSTR_SUCCESS or error code + */ +int nostr_nip42_validate_challenge(const char* challenge, + time_t received_at, + int time_tolerance); + +/** + * Parse AUTH challenge message from relay + * @param message Raw message from relay + * @param challenge_out Output buffer for challenge string + * @param challenge_size Size of challenge buffer + * @return NOSTR_SUCCESS or error code + */ +int nostr_nip42_parse_auth_challenge(const char* message, + char* challenge_out, + size_t challenge_size); + +// ============================================================================= +// SERVER-SIDE FUNCTIONS (for relay implementations) +// ============================================================================= + +/** + * Generate cryptographically secure challenge string + * @param challenge_out Output buffer for challenge (must be at least length*2+1) + * @param length Desired challenge length in bytes (16-128) + * @return NOSTR_SUCCESS or error code + */ +int nostr_nip42_generate_challenge(char* challenge_out, size_t length); + +/** + * Verify NIP-42 authentication event + * @param auth_event Authentication event to verify + * @param expected_challenge Challenge that was sent to client + * @param relay_url Expected relay URL + * @param time_tolerance Maximum timestamp deviation in seconds + * @return NOSTR_SUCCESS or error code + */ +int nostr_nip42_verify_auth_event(cJSON* auth_event, + const char* expected_challenge, + const char* relay_url, + int time_tolerance); + +/** + * Parse AUTH message from client + * @param message Raw AUTH message from client + * @param auth_event_out Output pointer to parsed event (caller must free) + * @return NOSTR_SUCCESS or error code + */ +int nostr_nip42_parse_auth_message(const char* message, cJSON** auth_event_out); + +/** + * Create "auth-required" error response + * @param subscription_id Subscription ID (for CLOSED) or NULL (for OK) + * @param event_id Event ID (for OK) or NULL (for CLOSED) + * @param reason Human-readable reason + * @return JSON string for response or NULL on error (caller must free) + */ +char* nostr_nip42_create_auth_required_message(const char* subscription_id, + const char* event_id, + const char* reason); + +/** + * Create "restricted" error response + * @param subscription_id Subscription ID (for CLOSED) or NULL (for OK) + * @param event_id Event ID (for OK) or NULL (for CLOSED) + * @param reason Human-readable reason + * @return JSON string for response or NULL on error (caller must free) + */ +char* nostr_nip42_create_restricted_message(const char* subscription_id, + const char* event_id, + const char* reason); + +// ============================================================================= +// URL NORMALIZATION FUNCTIONS +// ============================================================================= + +/** + * Normalize relay URL for comparison (removes trailing slashes, etc.) + * @param url Original URL + * @return Normalized URL string or NULL on error (caller must free) + */ +char* nostr_nip42_normalize_url(const char* url); + +/** + * Check if two relay URLs match after normalization + * @param url1 First URL + * @param url2 Second URL + * @return 1 if URLs match, 0 if they don't, -1 on error + */ +int nostr_nip42_urls_match(const char* url1, const char* url2); + +// ============================================================================= +// UTILITY FUNCTIONS +// ============================================================================= + +/** + * Get string description of authentication state + * @param state Authentication state + * @return Human-readable string + */ +const char* nostr_nip42_auth_state_str(nostr_auth_state_t state); + +/** + * Initialize authentication context structure + * @param ctx Context to initialize + * @param relay_url Relay URL + * @param challenge Challenge string + * @param time_tolerance Time tolerance in seconds + * @return NOSTR_SUCCESS or error code + */ +int nostr_nip42_init_auth_context(nostr_auth_context_t* ctx, + const char* relay_url, + const char* challenge, + int time_tolerance); + +/** + * Free authentication context structure + * @param ctx Context to free + */ +void nostr_nip42_free_auth_context(nostr_auth_context_t* ctx); + +/** + * Validate authentication event structure (without signature verification) + * @param auth_event Event to validate + * @param relay_url Expected relay URL + * @param challenge Expected challenge + * @param time_tolerance Maximum timestamp deviation in seconds + * @return NOSTR_SUCCESS or error code + */ +int nostr_nip42_validate_auth_event_structure(cJSON* auth_event, + const char* relay_url, + const char* challenge, + int time_tolerance); + +// ============================================================================= +// WEBSOCKET CLIENT INTEGRATION +// ============================================================================= + +// Forward declaration for WebSocket client +struct nostr_ws_client; + +/** + * Authenticate WebSocket client with relay + * @param client WebSocket client handle + * @param private_key 32-byte private key for authentication + * @param time_tolerance Maximum timestamp deviation in seconds (0 for default) + * @return NOSTR_SUCCESS or error code + */ +int nostr_ws_authenticate(struct nostr_ws_client* client, + const unsigned char* private_key, + int time_tolerance); + +/** + * Get current authentication state of WebSocket client + * @param client WebSocket client handle + * @return Current authentication state + */ +nostr_auth_state_t nostr_ws_get_auth_state(struct nostr_ws_client* client); + +/** + * Check if WebSocket client has stored valid challenge + * @param client WebSocket client handle + * @return 1 if valid challenge exists, 0 otherwise + */ +int nostr_ws_has_valid_challenge(struct nostr_ws_client* client); + +/** + * Get stored challenge from WebSocket client + * @param client WebSocket client handle + * @param challenge_out Output buffer for challenge + * @param challenge_size Size of output buffer + * @return NOSTR_SUCCESS or error code + */ +int nostr_ws_get_challenge(struct nostr_ws_client* client, + char* challenge_out, + size_t challenge_size); + +/** + * Store challenge in WebSocket client (internal function) + * @param client WebSocket client handle + * @param challenge Challenge string to store + * @return NOSTR_SUCCESS or error code + */ +int nostr_ws_store_challenge(struct nostr_ws_client* client, + const char* challenge); + +/** + * Clear authentication state in WebSocket client + * @param client WebSocket client handle + * @return NOSTR_SUCCESS or error code + */ +int nostr_ws_clear_auth_state(struct nostr_ws_client* client); + +#ifdef __cplusplus +} +#endif + +#endif // NIP042_H \ No newline at end of file diff --git a/nostr_core/nostr_common.c b/nostr_core/nostr_common.c index 8df544a6..6672cb3d 100644 --- a/nostr_core/nostr_common.c +++ b/nostr_core/nostr_common.c @@ -43,6 +43,15 @@ const char* nostr_strerror(int error_code) { case NOSTR_ERROR_NIP13_INVALID_NONCE_TAG: return "NIP-13: Invalid nonce tag format"; case NOSTR_ERROR_NIP13_TARGET_MISMATCH: return "NIP-13: Target difficulty mismatch"; case NOSTR_ERROR_NIP13_CALCULATION: return "NIP-13: PoW calculation error"; + case NOSTR_ERROR_NIP42_INVALID_CHALLENGE: return "NIP-42: Invalid challenge"; + case NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED: return "NIP-42: Challenge expired"; + case NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID: return "NIP-42: Authentication event invalid"; + case NOSTR_ERROR_NIP42_URL_MISMATCH: return "NIP-42: Relay URL mismatch"; + case NOSTR_ERROR_NIP42_TIME_TOLERANCE: return "NIP-42: Timestamp outside tolerance"; + case NOSTR_ERROR_NIP42_NOT_AUTHENTICATED: return "NIP-42: Client not authenticated"; + case NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT: return "NIP-42: Invalid message format"; + case NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT: return "NIP-42: Challenge too short"; + case NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG: return "NIP-42: Challenge too long"; default: return "Unknown error"; } } diff --git a/nostr_core/nostr_common.h b/nostr_core/nostr_common.h index f6eb37da..63b6701d 100644 --- a/nostr_core/nostr_common.h +++ b/nostr_core/nostr_common.h @@ -43,6 +43,16 @@ #define NOSTR_ERROR_NIP13_TARGET_MISMATCH -103 #define NOSTR_ERROR_NIP13_CALCULATION -104 +// NIP-42 Authentication-specific error codes +#define NOSTR_ERROR_NIP42_INVALID_CHALLENGE -200 +#define NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED -201 +#define NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID -202 +#define NOSTR_ERROR_NIP42_URL_MISMATCH -203 +#define NOSTR_ERROR_NIP42_TIME_TOLERANCE -204 +#define NOSTR_ERROR_NIP42_NOT_AUTHENTICATED -205 +#define NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT -206 +#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT -207 +#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG -208 // Constants #define NOSTR_PRIVATE_KEY_SIZE 32 diff --git a/nostr_core/nostr_core.h b/nostr_core/nostr_core.h index e5f2219f..9e3592ee 100644 --- a/nostr_core/nostr_core.h +++ b/nostr_core/nostr_core.h @@ -42,7 +42,14 @@ * - nostr_nip44_encrypt() -> Encrypt with ChaCha20 + HMAC * - nostr_nip44_encrypt_with_nonce() -> Encrypt with specific nonce (testing) * - nostr_nip44_decrypt() -> Decrypt ChaCha20 + HMAC messages - * + * + * NIP-42 AUTHENTICATION: + * - nostr_nip42_create_auth_event() -> Create authentication event (kind 22242) + * - nostr_nip42_verify_auth_event() -> Verify authentication event (relay-side) + * - nostr_nip42_generate_challenge() -> Generate challenge string (relay-side) + * - nostr_ws_authenticate() -> Authenticate WebSocket client + * - nostr_ws_get_auth_state() -> Get client authentication state + * * BIP39 MNEMONICS: * - nostr_bip39_mnemonic_from_bytes() -> Generate mnemonic from entropy * - nostr_bip39_mnemonic_validate() -> Validate mnemonic phrase @@ -96,7 +103,11 @@ * nostr_bip32_key_from_seed(seed, 64, &master_key); * uint32_t path[] = {44, 1237, 0, 0, 0}; // m/44'/1237'/0'/0/0 * nostr_bip32_derive_path(&master_key, path, 5, &derived_key); - * + * + * Client Authentication (NIP-42): + * cJSON* auth_event = nostr_nip42_create_auth_event(challenge, relay_url, private_key, 0); + * nostr_ws_authenticate(client, private_key, 600); // Auto-authenticate WebSocket + * * ============================================================================ */ @@ -116,6 +127,7 @@ extern "C" { #include "nip011.h" // Relay information document #include "nip013.h" // Proof of Work #include "nip019.h" // Bech32 encoding (nsec/npub) +#include "nip042.h" // Authentication of clients to relays #include "nip044.h" // Encryption (modern) // Relay communication functions are defined in nostr_common.h diff --git a/tests/bip32_test b/tests/bip32_test index 9e225174..69523c94 100755 Binary files a/tests/bip32_test and b/tests/bip32_test differ diff --git a/tests/nip01_test b/tests/nip01_test index ca7c9dcc..28cd57b4 100755 Binary files a/tests/nip01_test and b/tests/nip01_test differ diff --git a/tests/nip04_test b/tests/nip04_test index 8ddc4af0..5205e55b 100755 Binary files a/tests/nip04_test and b/tests/nip04_test differ diff --git a/tests/nip05_test b/tests/nip05_test index 32e585ae..c81c7a4f 100755 Binary files a/tests/nip05_test and b/tests/nip05_test differ diff --git a/tests/nip11_test b/tests/nip11_test index 8a4c77df..df07c7ec 100755 Binary files a/tests/nip11_test and b/tests/nip11_test differ diff --git a/tests/nip13_test b/tests/nip13_test index 2b4302e9..e2c3f35d 100755 Binary files a/tests/nip13_test and b/tests/nip13_test differ diff --git a/tests/nip42_test b/tests/nip42_test new file mode 100755 index 00000000..4d57a0ea Binary files /dev/null and b/tests/nip42_test differ diff --git a/tests/nip42_test.c b/tests/nip42_test.c new file mode 100644 index 00000000..cce90ae7 --- /dev/null +++ b/tests/nip42_test.c @@ -0,0 +1,690 @@ +/* + * NIP-42 Authentication of Clients to Relays Test Suite + * Tests auth challenge generation, event creation, validation, and message parsing + * Following TESTS POLICY: Shows expected vs actual values, prints entire JSON events + */ + +#define _GNU_SOURCE // For strdup on Linux +#include +#include +#include +#include +#include +#include "../nostr_core/nip042.h" +#include "../nostr_core/nip001.h" +#include "../nostr_core/nostr_common.h" +#include "../nostr_core/utils.h" +#include "../cjson/cJSON.h" + +// Ensure strdup is declared +#ifndef strdup +extern char *strdup(const char *s); +#endif + +// Test counter for tracking progress +static int test_count = 0; +static int passed_tests = 0; + +void print_test_header(const char* test_name) { + test_count++; + printf("\n=== TEST %d: %s ===\n", test_count, test_name); +} + +void print_test_result(int passed, const char* test_name) { + if (passed) { + passed_tests++; + printf("✅ PASS: %s\n", test_name); + } else { + printf("❌ FAIL: %s\n", test_name); + } +} + +void print_json_comparison(const char* label, cJSON* expected, cJSON* actual) { + char* expected_str = cJSON_Print(expected); + char* actual_str; + + if (actual) { + actual_str = cJSON_Print(actual); + } else { + actual_str = strdup("NULL"); + } + + printf("%s Expected JSON:\n%s\n", label, expected_str ? expected_str : "NULL"); + printf("%s Actual JSON:\n%s\n", label, actual_str ? actual_str : "NULL"); + + if (expected_str) free(expected_str); + if (actual_str) free(actual_str); +} + +// Test 1: Challenge generation +int test_challenge_generation(void) { + print_test_header("Challenge Generation"); + + nostr_auth_challenge_t challenge1, challenge2; + + printf("Generating first challenge...\n"); + int result1 = nostr_nip42_generate_challenge(challenge1.challenge, NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH); + printf("Result: %d (%s)\n", result1, nostr_strerror(result1)); + + if (result1 != NOSTR_SUCCESS) { + printf("❌ Failed to generate first challenge\n"); + return 0; + } + + printf("First challenge: %s\n", challenge1.challenge); + printf("Challenge length: %lu\n", strlen(challenge1.challenge)); + printf("Expected length: %d\n", NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH * 2); + + if (strlen(challenge1.challenge) != NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH * 2) { + printf("❌ Challenge length incorrect\n"); + return 0; + } + + printf("Generating second challenge...\n"); + int result2 = nostr_nip42_generate_challenge(challenge2.challenge, NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH); + printf("Result: %d (%s)\n", result2, nostr_strerror(result2)); + + if (result2 != NOSTR_SUCCESS) { + printf("❌ Failed to generate second challenge\n"); + return 0; + } + + printf("Second challenge: %s\n", challenge2.challenge); + + // Challenges should be different (extremely high probability) + if (strcmp(challenge1.challenge, challenge2.challenge) == 0) { + printf("❌ Two challenges are identical (highly unlikely)\n"); + return 0; + } + + printf("✅ Challenges are different (good entropy)\n"); + return 1; +} + +// Test 2: AUTH message creation (server-side challenge) +int test_auth_message_creation(void) { + print_test_header("AUTH Message Creation - Server Challenge"); + + nostr_auth_challenge_t challenge; + int gen_result = nostr_nip42_generate_challenge(challenge.challenge, NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH); + + if (gen_result != NOSTR_SUCCESS) { + printf("❌ Failed to generate challenge for message test\n"); + return 0; + } + + printf("Generated challenge: %s\n", challenge.challenge); + + // Create AUTH challenge message: ["AUTH", "challenge_string"] + cJSON* message_array = cJSON_CreateArray(); + if (!message_array) { + printf("❌ Failed to create message array\n"); + return 0; + } + + cJSON_AddItemToArray(message_array, cJSON_CreateString("AUTH")); + cJSON_AddItemToArray(message_array, cJSON_CreateString(challenge.challenge)); + + char* auth_message = cJSON_PrintUnformatted(message_array); + cJSON_Delete(message_array); + + if (!auth_message) { + printf("❌ Failed to create AUTH message\n"); + return 0; + } + + int result = NOSTR_SUCCESS; + + printf("Message creation result: %d (%s)\n", result, nostr_strerror(result)); + printf("AUTH message: %s\n", auth_message); + + // Parse the message to verify format + cJSON* parsed = cJSON_Parse(auth_message); + if (!parsed) { + printf("❌ AUTH message is not valid JSON\n"); + return 0; + } + + // Check if it's an array with 2 elements + if (!cJSON_IsArray(parsed) || cJSON_GetArraySize(parsed) != 2) { + printf("❌ AUTH message is not a 2-element array\n"); + cJSON_Delete(parsed); + return 0; + } + + // Check first element is "AUTH" + cJSON* first = cJSON_GetArrayItem(parsed, 0); + if (!cJSON_IsString(first) || strcmp(cJSON_GetStringValue(first), "AUTH") != 0) { + printf("❌ First element is not 'AUTH'\n"); + cJSON_Delete(parsed); + return 0; + } + + // Check second element is our challenge string + cJSON* second = cJSON_GetArrayItem(parsed, 1); + if (!cJSON_IsString(second) || strcmp(cJSON_GetStringValue(second), challenge.challenge) != 0) { + printf("❌ Second element is not our challenge string\n"); + printf("Expected: %s\n", challenge.challenge); + printf("Actual: %s\n", cJSON_GetStringValue(second)); + cJSON_Delete(parsed); + free(auth_message); + return 0; + } + + printf("✅ AUTH challenge message format is correct\n"); + cJSON_Delete(parsed); + free(auth_message); + return 1; +} + +// Test 3: Authentication event creation (client-side) +int test_auth_event_creation(void) { + print_test_header("Authentication Event Creation - Client Side"); + + const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"; + const char* relay_url = "wss://relay.example.com"; + const char* challenge_string = "test_challenge_12345678901234567890123456789012"; + + unsigned char private_key[32]; + nostr_hex_to_bytes(private_key_hex, private_key, 32); + + printf("Private key (hex): %s\n", private_key_hex); + printf("Relay URL: %s\n", relay_url); + printf("Challenge: %s\n", challenge_string); + + cJSON* auth_event = nostr_nip42_create_auth_event(challenge_string, relay_url, private_key, 0); + + if (!auth_event) { + printf("❌ Failed to create authentication event\n"); + return 0; + } + + char* event_str = cJSON_Print(auth_event); + printf("Created Auth Event JSON:\n%s\n", event_str); + free(event_str); + + // Validate the event structure + int structure_result = nostr_validate_event_structure(auth_event); + printf("Structure validation result: %d (%s)\n", structure_result, nostr_strerror(structure_result)); + + if (structure_result != NOSTR_SUCCESS) { + printf("❌ Auth event failed structure validation\n"); + cJSON_Delete(auth_event); + return 0; + } + + // Validate the event signature + int crypto_result = nostr_verify_event_signature(auth_event); + printf("Signature validation result: %d (%s)\n", crypto_result, nostr_strerror(crypto_result)); + + if (crypto_result != NOSTR_SUCCESS) { + printf("❌ Auth event failed signature validation\n"); + cJSON_Delete(auth_event); + return 0; + } + + // Check kind is 22242 + cJSON* kind = cJSON_GetObjectItem(auth_event, "kind"); + if (!cJSON_IsNumber(kind) || cJSON_GetNumberValue(kind) != 22242) { + printf("❌ Auth event kind is not 22242\n"); + cJSON_Delete(auth_event); + return 0; + } + + // Check for relay tag + cJSON* tags = cJSON_GetObjectItem(auth_event, "tags"); + int found_relay = 0, found_challenge = 0; + + if (cJSON_IsArray(tags)) { + cJSON* tag = NULL; + cJSON_ArrayForEach(tag, tags) { + if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { + cJSON* tag_name = cJSON_GetArrayItem(tag, 0); + cJSON* tag_value = cJSON_GetArrayItem(tag, 1); + + if (cJSON_IsString(tag_name) && cJSON_IsString(tag_value)) { + if (strcmp(cJSON_GetStringValue(tag_name), "relay") == 0) { + found_relay = 1; + if (strcmp(cJSON_GetStringValue(tag_value), relay_url) != 0) { + printf("❌ Relay tag value incorrect\n"); + printf("Expected: %s\n", relay_url); + printf("Actual: %s\n", cJSON_GetStringValue(tag_value)); + cJSON_Delete(auth_event); + return 0; + } + } else if (strcmp(cJSON_GetStringValue(tag_name), "challenge") == 0) { + found_challenge = 1; + if (strcmp(cJSON_GetStringValue(tag_value), challenge_string) != 0) { + printf("❌ Challenge tag value incorrect\n"); + printf("Expected: %s\n", challenge_string); + printf("Actual: %s\n", cJSON_GetStringValue(tag_value)); + cJSON_Delete(auth_event); + return 0; + } + } + } + } + } + } + + if (!found_relay) { + printf("❌ Missing relay tag\n"); + cJSON_Delete(auth_event); + return 0; + } + + if (!found_challenge) { + printf("❌ Missing challenge tag\n"); + cJSON_Delete(auth_event); + return 0; + } + + printf("✅ Authentication event created successfully with correct tags\n"); + cJSON_Delete(auth_event); + return 1; +} + +// Test 4: Authentication event validation (server-side) +int test_auth_event_validation(void) { + print_test_header("Authentication Event Validation - Server Side"); + + // Create a valid auth event first + const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"; + const char* relay_url = "wss://relay.example.com"; + const char* challenge_string = "validation_challenge_1234567890123456789012"; + + unsigned char private_key[32]; + nostr_hex_to_bytes(private_key_hex, private_key, 32); + + cJSON* auth_event = nostr_nip42_create_auth_event(challenge_string, relay_url, private_key, 0); + + if (!auth_event) { + printf("❌ Failed to create auth event for validation test\n"); + return 0; + } + + char* event_str = cJSON_Print(auth_event); + printf("Auth Event to validate:\n%s\n", event_str); + free(event_str); + + // Test successful validation + printf("Testing successful validation...\n"); + int result = nostr_nip42_verify_auth_event(auth_event, challenge_string, relay_url, 0); + printf("Validation result: %d (%s)\n", result, nostr_strerror(result)); + + if (result != NOSTR_SUCCESS) { + printf("❌ Valid auth event failed validation\n"); + cJSON_Delete(auth_event); + return 0; + } + printf("✅ Valid auth event passed validation\n"); + + // Test wrong relay URL + printf("\nTesting wrong relay URL...\n"); + result = nostr_nip42_verify_auth_event(auth_event, challenge_string, "wss://wrong.relay.com", 0); + printf("Expected: NOSTR_ERROR_NIP42_URL_MISMATCH (-206)\n"); + printf("Actual: %d (%s)\n", result, nostr_strerror(result)); + + if (result != NOSTR_ERROR_NIP42_URL_MISMATCH) { + printf("❌ Wrong relay validation didn't fail correctly\n"); + cJSON_Delete(auth_event); + return 0; + } + printf("✅ Wrong relay URL correctly rejected\n"); + + // Test wrong challenge + printf("\nTesting wrong challenge...\n"); + result = nostr_nip42_verify_auth_event(auth_event, "wrong_challenge_string_here", relay_url, 0); + printf("Expected: NOSTR_ERROR_NIP42_INVALID_CHALLENGE (-203)\n"); + printf("Actual: %d (%s)\n", result, nostr_strerror(result)); + + if (result != NOSTR_ERROR_NIP42_INVALID_CHALLENGE) { + printf("❌ Wrong challenge validation didn't fail correctly\n"); + cJSON_Delete(auth_event); + return 0; + } + printf("✅ Wrong challenge correctly rejected\n"); + + cJSON_Delete(auth_event); + return 1; +} + +// Test 5: AUTH message parsing (client-side) +int test_auth_message_parsing(void) { + print_test_header("AUTH Message Parsing - Client Side"); + + // Test parsing challenge message + const char* challenge_msg = "[\"AUTH\", \"test_challenge_from_server_123456789012\"]"; + printf("Parsing AUTH challenge message: %s\n", challenge_msg); + + char extracted_challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH]; + int result = nostr_nip42_parse_auth_challenge(challenge_msg, extracted_challenge, sizeof(extracted_challenge)); + + printf("Parse result: %d (%s)\n", result, nostr_strerror(result)); + printf("Expected challenge: test_challenge_from_server_123456789012\n"); + printf("Extracted challenge: %s\n", extracted_challenge); + + if (result != NOSTR_SUCCESS) { + printf("❌ Failed to parse valid AUTH message\n"); + return 0; + } + + if (strcmp(extracted_challenge, "test_challenge_from_server_123456789012") != 0) { + printf("❌ Extracted challenge doesn't match expected\n"); + return 0; + } + printf("✅ AUTH challenge message parsed correctly\n"); + + // Test invalid message format + printf("\nTesting invalid message format...\n"); + const char* invalid_msg = "[\"WRONG\", \"challenge\"]"; + result = nostr_nip42_parse_auth_challenge(invalid_msg, extracted_challenge, sizeof(extracted_challenge)); + + printf("Parse result: %d (%s)\n", result, nostr_strerror(result)); + printf("Expected: NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT (-205)\n"); + + if (result != NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT) { + printf("❌ Invalid message format should have failed\n"); + return 0; + } + printf("✅ Invalid message format correctly rejected\n"); + + return 1; +} + +// Test 6: AUTH response message creation (client-side) +int test_auth_response_creation(void) { + print_test_header("AUTH Response Message Creation - Client Side"); + + // Create an auth event first + const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"; + const char* relay_url = "wss://relay.example.com"; + const char* challenge_string = "response_test_challenge_1234567890123456"; + + unsigned char private_key[32]; + nostr_hex_to_bytes(private_key_hex, private_key, 32); + + cJSON* auth_event = nostr_nip42_create_auth_event(challenge_string, relay_url, private_key, 0); + + if (!auth_event) { + printf("❌ Failed to create auth event for response test\n"); + return 0; + } + + char* auth_response = nostr_nip42_create_auth_message(auth_event); + int result = auth_response ? NOSTR_SUCCESS : NOSTR_ERROR_MEMORY_FAILED; + + printf("Response creation result: %d (%s)\n", result, nostr_strerror(result)); + printf("AUTH response message: %s\n", auth_response ? auth_response : "NULL"); + + if (result != NOSTR_SUCCESS) { + printf("❌ Failed to create AUTH response message\n"); + cJSON_Delete(auth_event); + return 0; + } + + // Parse and validate the response format + cJSON* parsed = cJSON_Parse(auth_response); + if (!parsed) { + printf("❌ AUTH response is not valid JSON\n"); + cJSON_Delete(auth_event); + return 0; + } + + // Should be ["AUTH", ] + if (!cJSON_IsArray(parsed) || cJSON_GetArraySize(parsed) != 2) { + printf("❌ AUTH response is not a 2-element array\n"); + cJSON_Delete(parsed); + cJSON_Delete(auth_event); + return 0; + } + + cJSON* first = cJSON_GetArrayItem(parsed, 0); + if (!cJSON_IsString(first) || strcmp(cJSON_GetStringValue(first), "AUTH") != 0) { + printf("❌ First element is not 'AUTH'\n"); + cJSON_Delete(parsed); + cJSON_Delete(auth_event); + return 0; + } + + cJSON* second = cJSON_GetArrayItem(parsed, 1); + if (!cJSON_IsObject(second)) { + printf("❌ Second element is not an object (event)\n"); + cJSON_Delete(parsed); + cJSON_Delete(auth_event); + return 0; + } + + // Check if the event in the response matches our created event + cJSON* response_kind = cJSON_GetObjectItem(second, "kind"); + if (!cJSON_IsNumber(response_kind) || cJSON_GetNumberValue(response_kind) != 22242) { + printf("❌ Response event kind is not 22242\n"); + cJSON_Delete(parsed); + cJSON_Delete(auth_event); + return 0; + } + + printf("✅ AUTH response message format is correct\n"); + cJSON_Delete(parsed); + cJSON_Delete(auth_event); + free(auth_response); + return 1; +} + +// Test 7: Error conditions and edge cases +int test_error_conditions(void) { + print_test_header("Error Conditions and Edge Cases"); + + int all_passed = 1; + + // Test 1: NULL parameters + printf("\nSubtest 1: NULL parameters\n"); + int result = nostr_nip42_generate_challenge(NULL, 32); + printf("Expected: NOSTR_ERROR_INVALID_INPUT (-1)\n"); + printf("Actual: %d (%s)\n", result, nostr_strerror(result)); + if (result != NOSTR_ERROR_INVALID_INPUT) all_passed = 0; + + // Test 2: Invalid challenge length + printf("\nSubtest 2: Invalid challenge in validation\n"); + const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"; + const char* relay_url = "wss://relay.example.com"; + const char* valid_challenge = "valid_challenge_1234567890123456789012345"; + + unsigned char private_key[32]; + nostr_hex_to_bytes(private_key_hex, private_key, 32); + + cJSON* auth_event = nostr_nip42_create_auth_event(valid_challenge, relay_url, private_key, 0); + if (auth_event) { + result = nostr_nip42_verify_auth_event(auth_event, "short", relay_url, 0); // Too short + printf("Expected: NOSTR_ERROR_NIP42_INVALID_CHALLENGE (-203)\n"); + printf("Actual: %d (%s)\n", result, nostr_strerror(result)); + if (result != NOSTR_ERROR_NIP42_INVALID_CHALLENGE) all_passed = 0; + cJSON_Delete(auth_event); + } else { + printf("❌ Failed to create auth event for validation test\n"); + all_passed = 0; + } + + // Test 3: Invalid JSON parsing + printf("\nSubtest 3: Invalid JSON in message parsing\n"); + char challenge_buffer[NOSTR_NIP42_MAX_CHALLENGE_LENGTH]; + result = nostr_nip42_parse_auth_challenge("invalid json", challenge_buffer, sizeof(challenge_buffer)); + printf("Expected: NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT (-205)\n"); + printf("Actual: %d (%s)\n", result, nostr_strerror(result)); + if (result != NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT) all_passed = 0; + + // Test 4: Wrong array size + printf("\nSubtest 4: Wrong array size in message parsing\n"); + result = nostr_nip42_parse_auth_challenge("[\"AUTH\"]", challenge_buffer, sizeof(challenge_buffer)); // Only 1 element + printf("Expected: NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT (-205)\n"); + printf("Actual: %d (%s)\n", result, nostr_strerror(result)); + if (result != NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT) all_passed = 0; + + return all_passed; +} + +// Test 8: Full authentication flow simulation +int test_full_auth_flow(void) { + print_test_header("Full Authentication Flow Simulation"); + + const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"; + const char* relay_url = "wss://test-relay.nostr.com"; + + unsigned char private_key[32]; + nostr_hex_to_bytes(private_key_hex, private_key, 32); + + printf("=== STEP 1: Server generates challenge ===\n"); + nostr_auth_challenge_t challenge; + int result = nostr_nip42_generate_challenge(challenge.challenge, NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH); + if (result != NOSTR_SUCCESS) { + printf("❌ Failed to generate challenge\n"); + return 0; + } + printf("Generated challenge: %s\n", challenge.challenge); + + printf("\n=== STEP 2: Server sends AUTH message ===\n"); + cJSON* message_array = cJSON_CreateArray(); + cJSON_AddItemToArray(message_array, cJSON_CreateString("AUTH")); + cJSON_AddItemToArray(message_array, cJSON_CreateString(challenge.challenge)); + char* auth_message = cJSON_PrintUnformatted(message_array); + cJSON_Delete(message_array); + + if (!auth_message) { + printf("❌ Failed to create AUTH message\n"); + return 0; + } + printf("Server sends: %s\n", auth_message); + + printf("\n=== STEP 3: Client parses AUTH message ===\n"); + char parsed_challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH]; + result = nostr_nip42_parse_auth_challenge(auth_message, parsed_challenge, sizeof(parsed_challenge)); + if (result != NOSTR_SUCCESS) { + printf("❌ Failed to parse AUTH message\n"); + free(auth_message); + return 0; + } + printf("Client extracted challenge: %s\n", parsed_challenge); + + if (strcmp(challenge.challenge, parsed_challenge) != 0) { + printf("❌ Parsed challenge doesn't match original\n"); + free(auth_message); + return 0; + } + + printf("\n=== STEP 4: Client creates auth event ===\n"); + cJSON* auth_event = nostr_nip42_create_auth_event(parsed_challenge, relay_url, private_key, 0); + if (!auth_event) { + printf("❌ Failed to create auth event\n"); + return 0; + } + + char* event_str = cJSON_Print(auth_event); + printf("Client created auth event:\n%s\n", event_str); + free(event_str); + + printf("\n=== STEP 5: Client sends AUTH response ===\n"); + char* auth_response = nostr_nip42_create_auth_message(auth_event); + if (!auth_response) { + printf("❌ Failed to create AUTH response\n"); + cJSON_Delete(auth_event); + free(auth_message); + return 0; + } + printf("Client sends: %s\n", auth_response); + + printf("\n=== STEP 6: Server validates auth event ===\n"); + result = nostr_nip42_verify_auth_event(auth_event, challenge.challenge, relay_url, 0); + printf("Server validation result: %d (%s)\n", result, nostr_strerror(result)); + + if (result != NOSTR_SUCCESS) { + printf("❌ Server validation failed\n"); + cJSON_Delete(auth_event); + free(auth_message); + free(auth_response); + return 0; + } + + printf("✅ FULL AUTHENTICATION FLOW COMPLETED SUCCESSFULLY!\n"); + printf("✅ Challenge generated -> Message sent -> Challenge parsed -> Event created -> Response sent -> Event validated\n"); + + cJSON_Delete(auth_event); + free(auth_message); + free(auth_response); + return 1; +} + +int main(void) { + printf("=== NIP-42 Authentication of Clients to Relays Test Suite ===\n"); + printf("Following TESTS POLICY: Shows expected vs actual values, prints entire JSON events\n"); + printf("Tests both client-side and server-side authentication functionality\n"); + + // Initialize crypto library + if (nostr_init() != NOSTR_SUCCESS) { + printf("❌ Failed to initialize nostr library\n"); + return 1; + } + + int all_passed = 1; + int test_result; + + // Test 1: Challenge generation + test_result = test_challenge_generation(); + print_test_result(test_result, "Challenge Generation"); + if (!test_result) all_passed = 0; + + // Test 2: AUTH message creation + test_result = test_auth_message_creation(); + print_test_result(test_result, "AUTH Message Creation - Server Challenge"); + if (!test_result) all_passed = 0; + + // Test 3: Auth event creation + test_result = test_auth_event_creation(); + print_test_result(test_result, "Authentication Event Creation - Client Side"); + if (!test_result) all_passed = 0; + + // Test 4: Auth event validation + test_result = test_auth_event_validation(); + print_test_result(test_result, "Authentication Event Validation - Server Side"); + if (!test_result) all_passed = 0; + + // Test 5: AUTH message parsing + test_result = test_auth_message_parsing(); + print_test_result(test_result, "AUTH Message Parsing - Client Side"); + if (!test_result) all_passed = 0; + + // Test 6: AUTH response creation + test_result = test_auth_response_creation(); + print_test_result(test_result, "AUTH Response Message Creation - Client Side"); + if (!test_result) all_passed = 0; + + // Test 7: Error conditions + test_result = test_error_conditions(); + print_test_result(test_result, "Error Conditions and Edge Cases"); + if (!test_result) all_passed = 0; + + // Test 8: Full authentication flow + test_result = test_full_auth_flow(); + print_test_result(test_result, "Full Authentication Flow Simulation"); + if (!test_result) all_passed = 0; + + // Summary + printf("\n=== TEST SUMMARY ===\n"); + printf("Total tests: %d\n", test_count); + printf("Passed: %d\n", passed_tests); + printf("Failed: %d\n", test_count - passed_tests); + + if (all_passed) { + printf("🎉 ALL TESTS PASSED! NIP-42 Authentication implementation is working correctly.\n"); + printf("✅ Challenge generation works\n"); + printf("✅ AUTH message creation/parsing works\n"); + printf("✅ Authentication event creation works\n"); + printf("✅ Authentication event validation works\n"); + printf("✅ Full authentication flow works\n"); + printf("✅ Error handling works\n"); + } else { + printf("❌ SOME TESTS FAILED. Please review the output above.\n"); + } + + nostr_cleanup(); + return all_passed ? 0 : 1; +} \ No newline at end of file diff --git a/tests/nip44_test b/tests/nip44_test index 250c0c3a..11dd2c1d 100755 Binary files a/tests/nip44_test and b/tests/nip44_test differ diff --git a/tests/simple_init_test b/tests/simple_init_test index 6ebc3590..b71963fe 100755 Binary files a/tests/simple_init_test and b/tests/simple_init_test differ diff --git a/tests/sync_relay_test b/tests/sync_relay_test index d62fe1b0..bf776d27 100755 Binary files a/tests/sync_relay_test and b/tests/sync_relay_test differ diff --git a/tests/wss_test b/tests/wss_test index ddd01afb..dceda126 100755 Binary files a/tests/wss_test and b/tests/wss_test differ