/* * NOSTR Core Library - NIP-005: Mapping Nostr keys to DNS-based internet identifiers */ #include "nip005.h" #include "../cjson/cJSON.h" #include "nostr_common.h" #include #include #include #include // For strcasecmp #include #include // Structure for HTTP response handling typedef struct { char* data; size_t size; size_t capacity; } nip05_http_response_t; /** * Callback function for curl to write HTTP response data */ static size_t nip05_write_callback(void* contents, size_t size, size_t nmemb, void* userp) { nip05_http_response_t* response = (nip05_http_response_t*)userp; size_t total_size = size * nmemb; // Check if we need to expand the buffer if (response->size + total_size >= response->capacity) { size_t new_capacity = response->capacity * 2; if (new_capacity < response->size + total_size + 1) { new_capacity = response->size + total_size + 1; } char* new_data = realloc(response->data, new_capacity); if (!new_data) { return 0; // Out of memory } response->data = new_data; response->capacity = new_capacity; } // Append the new data memcpy(response->data + response->size, contents, total_size); response->size += total_size; response->data[response->size] = '\0'; return total_size; } /** * Parse and validate a NIP-05 identifier into local part and domain */ static int nip05_parse_identifier(const char* identifier, char* local_part, char* domain) { if (!identifier || !local_part || !domain) { return NOSTR_ERROR_INVALID_INPUT; } // Find the @ symbol const char* at_pos = strchr(identifier, '@'); if (!at_pos) { return NOSTR_ERROR_NIP05_INVALID_IDENTIFIER; } // Extract local part size_t local_len = at_pos - identifier; if (local_len == 0 || local_len >= 64) { return NOSTR_ERROR_NIP05_INVALID_IDENTIFIER; } strncpy(local_part, identifier, local_len); local_part[local_len] = '\0'; // Extract domain const char* domain_start = at_pos + 1; size_t domain_len = strlen(domain_start); if (domain_len == 0 || domain_len >= 256) { return NOSTR_ERROR_NIP05_INVALID_IDENTIFIER; } strcpy(domain, domain_start); // Validate characters in local part (a-z0-9-_.) for (size_t i = 0; i < local_len; i++) { char c = local_part[i]; if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.')) { return NOSTR_ERROR_NIP05_INVALID_IDENTIFIER; } } return NOSTR_SUCCESS; } /** * Make HTTP GET request to a URL */ static int nip05_http_get(const char* url, int timeout_seconds, char** response_data) { if (!url || !response_data) { return NOSTR_ERROR_INVALID_INPUT; } CURL* curl = curl_easy_init(); if (!curl) { return NOSTR_ERROR_NETWORK_FAILED; } nip05_http_response_t response = {0}; response.capacity = 1024; response.data = malloc(response.capacity); if (!response.data) { curl_easy_cleanup(curl); return NOSTR_ERROR_MEMORY_FAILED; } response.data[0] = '\0'; // Set curl options with proper type casting to fix warnings curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)nip05_write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)(timeout_seconds > 0 ? timeout_seconds : NIP05_DEFAULT_TIMEOUT)); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); // NIP-05 forbids redirects curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); curl_easy_setopt(curl, CURLOPT_USERAGENT, "nostr-core/1.0"); // Perform the request CURLcode res = curl_easy_perform(curl); long response_code = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); curl_easy_cleanup(curl); if (res != CURLE_OK) { free(response.data); return NOSTR_ERROR_NIP05_HTTP_FAILED; } if (response_code != 200) { free(response.data); return NOSTR_ERROR_NIP05_HTTP_FAILED; } *response_data = response.data; return NOSTR_SUCCESS; } /** * Validate that a hex string is a valid public key */ static int nip05_validate_pubkey_hex(const char* hex_pubkey) { if (!hex_pubkey) { return NOSTR_ERROR_INVALID_INPUT; } size_t len = strlen(hex_pubkey); if (len != 64) { return NOSTR_ERROR_INVALID_INPUT; } // Check all characters are valid hex for (size_t i = 0; i < len; i++) { char c = hex_pubkey[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { return NOSTR_ERROR_INVALID_INPUT; } } return NOSTR_SUCCESS; } /** * Parse a .well-known/nostr.json response and extract pubkey and relays for a specific name */ int nostr_nip05_parse_well_known(const char* json_response, const char* local_part, char* pubkey_hex_out, char*** relays, int* relay_count) { if (!json_response || !local_part) { return NOSTR_ERROR_INVALID_INPUT; } // Initialize outputs if (pubkey_hex_out) { pubkey_hex_out[0] = '\0'; } if (relays) { *relays = NULL; } if (relay_count) { *relay_count = 0; } // Parse JSON cJSON* json = cJSON_Parse(json_response); if (!json) { return NOSTR_ERROR_NIP05_JSON_PARSE_FAILED; } int result = NOSTR_ERROR_NIP05_NAME_NOT_FOUND; // Get the "names" object cJSON* names = cJSON_GetObjectItem(json, "names"); if (names && cJSON_IsObject(names)) { cJSON* pubkey_item = cJSON_GetObjectItem(names, local_part); if (pubkey_item && cJSON_IsString(pubkey_item)) { const char* found_pubkey = cJSON_GetStringValue(pubkey_item); // Validate the public key format if (nip05_validate_pubkey_hex(found_pubkey) == NOSTR_SUCCESS) { if (pubkey_hex_out) { strcpy(pubkey_hex_out, found_pubkey); } result = NOSTR_SUCCESS; // Extract relays if requested if (relays && relay_count) { cJSON* relays_obj = cJSON_GetObjectItem(json, "relays"); if (relays_obj && cJSON_IsObject(relays_obj)) { cJSON* user_relays = cJSON_GetObjectItem(relays_obj, found_pubkey); if (user_relays && cJSON_IsArray(user_relays)) { int relay_array_size = cJSON_GetArraySize(user_relays); if (relay_array_size > 0) { char** relay_array = malloc(relay_array_size * sizeof(char*)); if (relay_array) { int valid_relays = 0; for (int i = 0; i < relay_array_size; i++) { cJSON* relay_item = cJSON_GetArrayItem(user_relays, i); if (relay_item && cJSON_IsString(relay_item)) { const char* relay_url = cJSON_GetStringValue(relay_item); if (relay_url && strlen(relay_url) > 0) { relay_array[valid_relays] = malloc(strlen(relay_url) + 1); if (relay_array[valid_relays]) { strcpy(relay_array[valid_relays], relay_url); valid_relays++; } } } } if (valid_relays > 0) { *relays = relay_array; *relay_count = valid_relays; } else { free(relay_array); } } } } } } } } } cJSON_Delete(json); return result; } /** * Lookup a public key from a NIP-05 identifier */ int nostr_nip05_lookup(const char* nip05_identifier, char* pubkey_hex_out, char*** relays, int* relay_count, int timeout_seconds) { if (!nip05_identifier) { return NOSTR_ERROR_INVALID_INPUT; } char local_part[64]; char domain[256]; char url[NOSTR_MAX_URL_SIZE]; // Parse the identifier int parse_result = nip05_parse_identifier(nip05_identifier, local_part, domain); if (parse_result != NOSTR_SUCCESS) { return parse_result; } // Construct the .well-known URL int url_result = snprintf(url, sizeof(url), "https://%s/.well-known/nostr.json?name=%s", domain, local_part); if (url_result >= (int)sizeof(url) || url_result < 0) { return NOSTR_ERROR_INVALID_INPUT; } // Make HTTP request char* response_data = NULL; int http_result = nip05_http_get(url, timeout_seconds, &response_data); if (http_result != NOSTR_SUCCESS) { return http_result; } // Parse the response int parse_response_result = nostr_nip05_parse_well_known(response_data, local_part, pubkey_hex_out, relays, relay_count); free(response_data); return parse_response_result; } /** * Verify a NIP-05 identifier against a public key */ int nostr_nip05_verify(const char* nip05_identifier, const char* pubkey_hex, char*** relays, int* relay_count, int timeout_seconds) { if (!nip05_identifier || !pubkey_hex) { return NOSTR_ERROR_INVALID_INPUT; } // Validate the input public key format if (nip05_validate_pubkey_hex(pubkey_hex) != NOSTR_SUCCESS) { return NOSTR_ERROR_INVALID_INPUT; } char found_pubkey[65]; // Lookup the public key for this identifier int lookup_result = nostr_nip05_lookup(nip05_identifier, found_pubkey, relays, relay_count, timeout_seconds); if (lookup_result != NOSTR_SUCCESS) { return lookup_result; } // Compare the public keys (case insensitive) if (strcasecmp(pubkey_hex, found_pubkey) != 0) { // Clean up relays if verification failed if (relays && *relays) { for (int i = 0; i < (relay_count ? *relay_count : 0); i++) { free((*relays)[i]); } free(*relays); *relays = NULL; } if (relay_count) { *relay_count = 0; } return NOSTR_ERROR_NIP05_PUBKEY_MISMATCH; } return NOSTR_SUCCESS; }