344 lines
11 KiB
C
344 lines
11 KiB
C
/*
|
|
* 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 <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h> // For strcasecmp
|
|
#include <ctype.h>
|
|
|
|
|
|
#include <curl/curl.h>
|
|
|
|
|
|
// 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;
|
|
} |