Initial template structure from nostr_core_lib
- Complete C library template with OpenSSL-based crypto - Comprehensive build system (Makefile, build.sh) - Example code and test suite - Documentation and usage guides - Cross-platform compatibility (x64/ARM64) - Production-ready structure for C library projects
This commit is contained in:
@@ -4,11 +4,12 @@
|
||||
* Self-contained crypto implementation (no external crypto dependencies)
|
||||
*
|
||||
* This file contains:
|
||||
* - NIP-19: Bech32-encoded Entities
|
||||
* - NIP-01: Basic Protocol Flow
|
||||
* - NIP-05: Mapping Nostr keys to DNS-based internet identifiers
|
||||
* - NIP-06: Key Derivation from Mnemonic
|
||||
* - NIP-10: Text Notes (Kind 1)
|
||||
* - NIP-13: Proof of Work
|
||||
* - NIP-19: Bech32-encoded Entities
|
||||
* - General Utilities
|
||||
* - Identity Management
|
||||
* - Single Relay Communication
|
||||
@@ -35,6 +36,9 @@
|
||||
// cJSON for JSON handling
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
#ifndef DISABLE_NIP05
|
||||
#include <curl/curl.h>
|
||||
#endif
|
||||
|
||||
// Forward declarations for bech32 functions (used by NIP-06 functions)
|
||||
static int convert_bits(uint8_t *out, size_t *outlen, int outbits, const uint8_t *in, size_t inlen, int inbits, int pad);
|
||||
@@ -98,6 +102,11 @@ const char* nostr_strerror(int error_code) {
|
||||
case NOSTR_ERROR_NIP04_INVALID_FORMAT: return "NIP-04 invalid format";
|
||||
case NOSTR_ERROR_NIP04_DECRYPT_FAILED: return "NIP-04 decryption failed";
|
||||
case NOSTR_ERROR_NIP04_BUFFER_TOO_SMALL: return "NIP-04 buffer too small";
|
||||
case NOSTR_ERROR_NIP05_INVALID_IDENTIFIER: return "NIP-05: Invalid identifier format";
|
||||
case NOSTR_ERROR_NIP05_HTTP_FAILED: return "NIP-05: HTTP request failed";
|
||||
case NOSTR_ERROR_NIP05_JSON_PARSE_FAILED: return "NIP-05: JSON parsing failed";
|
||||
case NOSTR_ERROR_NIP05_NAME_NOT_FOUND: return "NIP-05: Name not found in .well-known";
|
||||
case NOSTR_ERROR_NIP05_PUBKEY_MISMATCH: return "NIP-05: Public key mismatch";
|
||||
default: return "Unknown error";
|
||||
}
|
||||
}
|
||||
@@ -216,6 +225,820 @@ cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, c
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// NIP-05: MAPPING NOSTR KEYS TO DNS-BASED INTERNET IDENTIFIERS
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef DISABLE_NIP05
|
||||
|
||||
// Maximum sizes for NIP-05 operations
|
||||
#define NIP05_MAX_URL_SIZE 512
|
||||
#define NIP05_MAX_RESPONSE_SIZE 8192
|
||||
#define NIP05_MAX_IDENTIFIER_SIZE 256
|
||||
#define NIP05_DEFAULT_TIMEOUT 10
|
||||
|
||||
// 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, nip05_http_response_t* response) {
|
||||
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
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, nip05_write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 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[NIP05_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;
|
||||
}
|
||||
|
||||
#else // DISABLE_NIP05
|
||||
|
||||
/**
|
||||
* Stub implementations when NIP-05 is disabled at compile time
|
||||
*/
|
||||
int nostr_nip05_parse_well_known(const char* json_response, const char* local_part,
|
||||
char* pubkey_hex_out, char*** relays, int* relay_count) {
|
||||
(void)json_response;
|
||||
(void)local_part;
|
||||
(void)pubkey_hex_out;
|
||||
(void)relays;
|
||||
(void)relay_count;
|
||||
return NOSTR_ERROR_NETWORK_FAILED; // NIP-05 disabled at compile time
|
||||
}
|
||||
|
||||
int nostr_nip05_lookup(const char* nip05_identifier, char* pubkey_hex_out,
|
||||
char*** relays, int* relay_count, int timeout_seconds) {
|
||||
(void)nip05_identifier;
|
||||
(void)pubkey_hex_out;
|
||||
(void)relays;
|
||||
(void)relay_count;
|
||||
(void)timeout_seconds;
|
||||
return NOSTR_ERROR_NETWORK_FAILED; // NIP-05 disabled at compile time
|
||||
}
|
||||
|
||||
int nostr_nip05_verify(const char* nip05_identifier, const char* pubkey_hex,
|
||||
char*** relays, int* relay_count, int timeout_seconds) {
|
||||
(void)nip05_identifier;
|
||||
(void)pubkey_hex;
|
||||
(void)relays;
|
||||
(void)relay_count;
|
||||
(void)timeout_seconds;
|
||||
return NOSTR_ERROR_NETWORK_FAILED; // NIP-05 disabled at compile time
|
||||
}
|
||||
|
||||
#endif // DISABLE_NIP05
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// NIP-11: RELAY INFORMATION DOCUMENT
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef DISABLE_NIP05 // NIP-11 uses the same HTTP infrastructure as NIP-05
|
||||
|
||||
// Maximum sizes for NIP-11 operations
|
||||
#define NIP11_MAX_URL_SIZE 512
|
||||
#define NIP11_MAX_RESPONSE_SIZE 16384
|
||||
#define NIP11_DEFAULT_TIMEOUT 10
|
||||
|
||||
// Forward declaration
|
||||
void nostr_nip11_relay_info_free(nostr_relay_info_t* info);
|
||||
|
||||
/**
|
||||
* Convert WebSocket URL to HTTP URL for NIP-11 document retrieval
|
||||
*/
|
||||
static char* nip11_ws_to_http_url(const char* ws_url) {
|
||||
if (!ws_url) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t url_len = strlen(ws_url);
|
||||
char* http_url = malloc(url_len + 10); // Extra space for protocol change
|
||||
if (!http_url) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Convert ws:// to http:// and wss:// to https://
|
||||
if (strncmp(ws_url, "ws://", 5) == 0) {
|
||||
sprintf(http_url, "http://%s", ws_url + 5);
|
||||
} else if (strncmp(ws_url, "wss://", 6) == 0) {
|
||||
sprintf(http_url, "https://%s", ws_url + 6);
|
||||
} else {
|
||||
// Assume it's already HTTP(S) or add https:// as default
|
||||
if (strncmp(ws_url, "http://", 7) == 0 || strncmp(ws_url, "https://", 8) == 0) {
|
||||
strcpy(http_url, ws_url);
|
||||
} else {
|
||||
sprintf(http_url, "https://%s", ws_url);
|
||||
}
|
||||
}
|
||||
|
||||
return http_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse supported NIPs array from JSON
|
||||
*/
|
||||
static int nip11_parse_supported_nips(cJSON* nips_array, int** nips_out, size_t* count_out) {
|
||||
if (!nips_array || !cJSON_IsArray(nips_array)) {
|
||||
*nips_out = NULL;
|
||||
*count_out = 0;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int array_size = cJSON_GetArraySize(nips_array);
|
||||
if (array_size == 0) {
|
||||
*nips_out = NULL;
|
||||
*count_out = 0;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int* nips = malloc(array_size * sizeof(int));
|
||||
if (!nips) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
int valid_count = 0;
|
||||
for (int i = 0; i < array_size; i++) {
|
||||
cJSON* nip_item = cJSON_GetArrayItem(nips_array, i);
|
||||
if (nip_item && cJSON_IsNumber(nip_item)) {
|
||||
nips[valid_count++] = (int)cJSON_GetNumberValue(nip_item);
|
||||
}
|
||||
}
|
||||
|
||||
if (valid_count == 0) {
|
||||
free(nips);
|
||||
*nips_out = NULL;
|
||||
*count_out = 0;
|
||||
} else {
|
||||
*nips_out = nips;
|
||||
*count_out = valid_count;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse string array from JSON
|
||||
*/
|
||||
static int nip11_parse_string_array(cJSON* json_array, char*** strings_out, size_t* count_out) {
|
||||
if (!json_array || !cJSON_IsArray(json_array)) {
|
||||
*strings_out = NULL;
|
||||
*count_out = 0;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int array_size = cJSON_GetArraySize(json_array);
|
||||
if (array_size == 0) {
|
||||
*strings_out = NULL;
|
||||
*count_out = 0;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
char** strings = malloc(array_size * sizeof(char*));
|
||||
if (!strings) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
int valid_count = 0;
|
||||
for (int i = 0; i < array_size; i++) {
|
||||
cJSON* string_item = cJSON_GetArrayItem(json_array, i);
|
||||
if (string_item && cJSON_IsString(string_item)) {
|
||||
const char* str_value = cJSON_GetStringValue(string_item);
|
||||
if (str_value && strlen(str_value) > 0) {
|
||||
strings[valid_count] = malloc(strlen(str_value) + 1);
|
||||
if (strings[valid_count]) {
|
||||
strcpy(strings[valid_count], str_value);
|
||||
valid_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (valid_count == 0) {
|
||||
free(strings);
|
||||
*strings_out = NULL;
|
||||
*count_out = 0;
|
||||
} else {
|
||||
*strings_out = strings;
|
||||
*count_out = valid_count;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to safely copy JSON string value
|
||||
*/
|
||||
static char* nip11_copy_json_string(cJSON* json_item) {
|
||||
if (!json_item || !cJSON_IsString(json_item)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char* str_value = cJSON_GetStringValue(json_item);
|
||||
if (!str_value) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* copy = malloc(strlen(str_value) + 1);
|
||||
if (copy) {
|
||||
strcpy(copy, str_value);
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse NIP-11 relay information document from JSON
|
||||
*/
|
||||
static int nip11_parse_relay_info(const char* json_response, nostr_relay_info_t* info) {
|
||||
if (!json_response || !info) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
// Initialize structure
|
||||
memset(info, 0, sizeof(nostr_relay_info_t));
|
||||
|
||||
// Parse JSON
|
||||
cJSON* json = cJSON_Parse(json_response);
|
||||
if (!json) {
|
||||
return NOSTR_ERROR_NIP05_JSON_PARSE_FAILED;
|
||||
}
|
||||
|
||||
// Parse basic information
|
||||
info->basic.name = nip11_copy_json_string(cJSON_GetObjectItem(json, "name"));
|
||||
info->basic.description = nip11_copy_json_string(cJSON_GetObjectItem(json, "description"));
|
||||
info->basic.pubkey = nip11_copy_json_string(cJSON_GetObjectItem(json, "pubkey"));
|
||||
info->basic.contact = nip11_copy_json_string(cJSON_GetObjectItem(json, "contact"));
|
||||
info->basic.software = nip11_copy_json_string(cJSON_GetObjectItem(json, "software"));
|
||||
info->basic.version = nip11_copy_json_string(cJSON_GetObjectItem(json, "version"));
|
||||
|
||||
// Parse supported NIPs
|
||||
cJSON* supported_nips = cJSON_GetObjectItem(json, "supported_nips");
|
||||
nip11_parse_supported_nips(supported_nips, &info->basic.supported_nips, &info->basic.supported_nips_count);
|
||||
|
||||
// Parse limitations (if present)
|
||||
cJSON* limitation = cJSON_GetObjectItem(json, "limitation");
|
||||
if (limitation && cJSON_IsObject(limitation)) {
|
||||
info->has_limitations = 1;
|
||||
|
||||
cJSON* item;
|
||||
item = cJSON_GetObjectItem(limitation, "max_message_length");
|
||||
info->limitations.max_message_length = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
||||
|
||||
item = cJSON_GetObjectItem(limitation, "max_subscriptions");
|
||||
info->limitations.max_subscriptions = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
||||
|
||||
item = cJSON_GetObjectItem(limitation, "max_filters");
|
||||
info->limitations.max_filters = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
||||
|
||||
item = cJSON_GetObjectItem(limitation, "max_limit");
|
||||
info->limitations.max_limit = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
||||
|
||||
item = cJSON_GetObjectItem(limitation, "max_subid_length");
|
||||
info->limitations.max_subid_length = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
||||
|
||||
item = cJSON_GetObjectItem(limitation, "min_prefix");
|
||||
info->limitations.min_prefix = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
||||
|
||||
item = cJSON_GetObjectItem(limitation, "max_event_tags");
|
||||
info->limitations.max_event_tags = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
||||
|
||||
item = cJSON_GetObjectItem(limitation, "max_content_length");
|
||||
info->limitations.max_content_length = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
||||
|
||||
item = cJSON_GetObjectItem(limitation, "min_pow_difficulty");
|
||||
info->limitations.min_pow_difficulty = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
||||
|
||||
item = cJSON_GetObjectItem(limitation, "auth_required");
|
||||
info->limitations.auth_required = (item && cJSON_IsBool(item)) ? (cJSON_IsTrue(item) ? 1 : 0) : -1;
|
||||
|
||||
item = cJSON_GetObjectItem(limitation, "payment_required");
|
||||
info->limitations.payment_required = (item && cJSON_IsBool(item)) ? (cJSON_IsTrue(item) ? 1 : 0) : -1;
|
||||
|
||||
item = cJSON_GetObjectItem(limitation, "restricted_writes");
|
||||
info->limitations.restricted_writes = (item && cJSON_IsBool(item)) ? (cJSON_IsTrue(item) ? 1 : 0) : -1;
|
||||
|
||||
item = cJSON_GetObjectItem(limitation, "created_at_lower_limit");
|
||||
info->limitations.created_at_lower_limit = (item && cJSON_IsNumber(item)) ? (long)cJSON_GetNumberValue(item) : -1;
|
||||
|
||||
item = cJSON_GetObjectItem(limitation, "created_at_upper_limit");
|
||||
info->limitations.created_at_upper_limit = (item && cJSON_IsNumber(item)) ? (long)cJSON_GetNumberValue(item) : -1;
|
||||
}
|
||||
|
||||
// Parse relay countries
|
||||
cJSON* relay_countries = cJSON_GetObjectItem(json, "relay_countries");
|
||||
if (relay_countries && cJSON_IsArray(relay_countries)) {
|
||||
info->has_content_limitations = 1;
|
||||
nip11_parse_string_array(relay_countries, &info->content_limitations.relay_countries,
|
||||
&info->content_limitations.relay_countries_count);
|
||||
}
|
||||
|
||||
// Parse community preferences
|
||||
cJSON* language_tags = cJSON_GetObjectItem(json, "language_tags");
|
||||
cJSON* tags = cJSON_GetObjectItem(json, "tags");
|
||||
cJSON* posting_policy = cJSON_GetObjectItem(json, "posting_policy");
|
||||
|
||||
if ((language_tags && cJSON_IsArray(language_tags)) ||
|
||||
(tags && cJSON_IsArray(tags)) ||
|
||||
(posting_policy && cJSON_IsString(posting_policy))) {
|
||||
info->has_community_preferences = 1;
|
||||
|
||||
if (language_tags) {
|
||||
nip11_parse_string_array(language_tags, &info->community_preferences.language_tags,
|
||||
&info->community_preferences.language_tags_count);
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
nip11_parse_string_array(tags, &info->community_preferences.tags,
|
||||
&info->community_preferences.tags_count);
|
||||
}
|
||||
|
||||
if (posting_policy) {
|
||||
info->community_preferences.posting_policy = nip11_copy_json_string(posting_policy);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse icon
|
||||
cJSON* icon = cJSON_GetObjectItem(json, "icon");
|
||||
if (icon && cJSON_IsString(icon)) {
|
||||
info->has_icon = 1;
|
||||
info->icon.icon = nip11_copy_json_string(icon);
|
||||
}
|
||||
|
||||
cJSON_Delete(json);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch relay information document from a relay URL
|
||||
*/
|
||||
int nostr_nip11_fetch_relay_info(const char* relay_url, nostr_relay_info_t** info_out, int timeout_seconds) {
|
||||
if (!relay_url || !info_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
// Convert WebSocket URL to HTTP URL
|
||||
char* http_url = nip11_ws_to_http_url(relay_url);
|
||||
if (!http_url) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
// Allocate info structure
|
||||
nostr_relay_info_t* info = malloc(sizeof(nostr_relay_info_t));
|
||||
if (!info) {
|
||||
free(http_url);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
// Make HTTP request with NIP-11 required header
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
free(http_url);
|
||||
free(info);
|
||||
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);
|
||||
free(http_url);
|
||||
free(info);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
response.data[0] = '\0';
|
||||
|
||||
// Set up headers for NIP-11
|
||||
struct curl_slist* headers = NULL;
|
||||
headers = curl_slist_append(headers, "Accept: application/nostr+json");
|
||||
|
||||
// Set curl options
|
||||
curl_easy_setopt(curl, CURLOPT_URL, http_url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, nip05_write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout_seconds > 0 ? timeout_seconds : NIP11_DEFAULT_TIMEOUT);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); // NIP-11 allows redirects
|
||||
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L);
|
||||
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_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
free(http_url);
|
||||
|
||||
if (res != CURLE_OK || response_code != 200) {
|
||||
free(response.data);
|
||||
free(info);
|
||||
return NOSTR_ERROR_NIP05_HTTP_FAILED;
|
||||
}
|
||||
|
||||
// Parse the relay information
|
||||
int parse_result = nip11_parse_relay_info(response.data, info);
|
||||
free(response.data);
|
||||
|
||||
if (parse_result != NOSTR_SUCCESS) {
|
||||
nostr_nip11_relay_info_free(info);
|
||||
return parse_result;
|
||||
}
|
||||
|
||||
*info_out = info;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Free relay information structure
|
||||
*/
|
||||
void nostr_nip11_relay_info_free(nostr_relay_info_t* info) {
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Free basic info strings
|
||||
free(info->basic.name);
|
||||
free(info->basic.description);
|
||||
free(info->basic.pubkey);
|
||||
free(info->basic.contact);
|
||||
free(info->basic.software);
|
||||
free(info->basic.version);
|
||||
free(info->basic.supported_nips);
|
||||
|
||||
// Free content limitations
|
||||
if (info->has_content_limitations) {
|
||||
for (size_t i = 0; i < info->content_limitations.relay_countries_count; i++) {
|
||||
free(info->content_limitations.relay_countries[i]);
|
||||
}
|
||||
free(info->content_limitations.relay_countries);
|
||||
}
|
||||
|
||||
// Free community preferences
|
||||
if (info->has_community_preferences) {
|
||||
for (size_t i = 0; i < info->community_preferences.language_tags_count; i++) {
|
||||
free(info->community_preferences.language_tags[i]);
|
||||
}
|
||||
free(info->community_preferences.language_tags);
|
||||
|
||||
for (size_t i = 0; i < info->community_preferences.tags_count; i++) {
|
||||
free(info->community_preferences.tags[i]);
|
||||
}
|
||||
free(info->community_preferences.tags);
|
||||
|
||||
free(info->community_preferences.posting_policy);
|
||||
}
|
||||
|
||||
// Free icon
|
||||
if (info->has_icon) {
|
||||
free(info->icon.icon);
|
||||
}
|
||||
|
||||
free(info);
|
||||
}
|
||||
|
||||
#else // DISABLE_NIP05
|
||||
|
||||
/**
|
||||
* Stub implementations when NIP-05 is disabled at compile time
|
||||
*/
|
||||
int nostr_nip11_fetch_relay_info(const char* relay_url, nostr_relay_info_t** info_out, int timeout_seconds) {
|
||||
(void)relay_url;
|
||||
(void)info_out;
|
||||
(void)timeout_seconds;
|
||||
return NOSTR_ERROR_NETWORK_FAILED; // NIP-11 disabled at compile time
|
||||
}
|
||||
|
||||
void nostr_nip11_relay_info_free(nostr_relay_info_t* info) {
|
||||
(void)info;
|
||||
// No-op when disabled
|
||||
}
|
||||
|
||||
#endif // DISABLE_NIP05
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// NIP-06: KEY DERIVATION FROM MNEMONIC
|
||||
|
||||
Binary file not shown.
@@ -36,6 +36,11 @@ typedef struct cJSON cJSON;
|
||||
#define NOSTR_ERROR_NIP44_INVALID_FORMAT -13
|
||||
#define NOSTR_ERROR_NIP44_DECRYPT_FAILED -14
|
||||
#define NOSTR_ERROR_NIP44_BUFFER_TOO_SMALL -15
|
||||
#define NOSTR_ERROR_NIP05_INVALID_IDENTIFIER -16
|
||||
#define NOSTR_ERROR_NIP05_HTTP_FAILED -17
|
||||
#define NOSTR_ERROR_NIP05_JSON_PARSE_FAILED -18
|
||||
#define NOSTR_ERROR_NIP05_NAME_NOT_FOUND -19
|
||||
#define NOSTR_ERROR_NIP05_PUBKEY_MISMATCH -20
|
||||
|
||||
// Debug control - uncomment to enable debug output
|
||||
// #define NOSTR_DEBUG_ENABLED
|
||||
@@ -163,6 +168,147 @@ cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, c
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// NIP-05: MAPPING NOSTR KEYS TO DNS-BASED INTERNET IDENTIFIERS
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Verify a NIP-05 identifier against a public key
|
||||
* Checks if the given identifier (e.g., "bob@example.com") maps to the provided pubkey
|
||||
*
|
||||
* @param nip05_identifier Internet identifier (e.g., "bob@example.com")
|
||||
* @param pubkey_hex Public key in hex format to verify against
|
||||
* @param relays OUTPUT: Array of relay URLs (caller must free each string and array), NULL if not needed
|
||||
* @param relay_count OUTPUT: Number of relay URLs returned, NULL if not needed
|
||||
* @param timeout_seconds HTTP timeout in seconds (0 for default 10 seconds)
|
||||
* @return NOSTR_SUCCESS if verified, NOSTR_ERROR_* on failure
|
||||
*/
|
||||
int nostr_nip05_verify(const char* nip05_identifier, const char* pubkey_hex,
|
||||
char*** relays, int* relay_count, int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Lookup a public key from a NIP-05 identifier
|
||||
* Finds the public key associated with the given identifier (e.g., "bob@example.com")
|
||||
*
|
||||
* @param nip05_identifier Internet identifier (e.g., "bob@example.com")
|
||||
* @param pubkey_hex_out OUTPUT: Public key in hex format (65 bytes including null terminator)
|
||||
* @param relays OUTPUT: Array of relay URLs (caller must free each string and array), NULL if not needed
|
||||
* @param relay_count OUTPUT: Number of relay URLs returned, NULL if not needed
|
||||
* @param timeout_seconds HTTP timeout in seconds (0 for default 10 seconds)
|
||||
* @return NOSTR_SUCCESS if found, NOSTR_ERROR_* on failure
|
||||
*/
|
||||
int nostr_nip05_lookup(const char* nip05_identifier, char* pubkey_hex_out,
|
||||
char*** relays, int* relay_count, int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Parse a .well-known/nostr.json response and extract pubkey and relays for a specific name
|
||||
*
|
||||
* @param json_response JSON string from .well-known/nostr.json endpoint
|
||||
* @param local_part Local part of identifier (e.g., "bob" from "bob@example.com")
|
||||
* @param pubkey_hex_out OUTPUT: Public key in hex format (65 bytes including null terminator)
|
||||
* @param relays OUTPUT: Array of relay URLs (caller must free each string and array), NULL if not needed
|
||||
* @param relay_count OUTPUT: Number of relay URLs returned, NULL if not needed
|
||||
* @return NOSTR_SUCCESS if found, NOSTR_ERROR_* on failure
|
||||
*/
|
||||
int nostr_nip05_parse_well_known(const char* json_response, const char* local_part,
|
||||
char* pubkey_hex_out, char*** relays, int* relay_count);
|
||||
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// NIP-11: RELAY INFORMATION DOCUMENT
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// NIP-11 data structures
|
||||
typedef struct {
|
||||
char* name;
|
||||
char* description;
|
||||
char* pubkey;
|
||||
char* contact;
|
||||
int* supported_nips;
|
||||
size_t supported_nips_count;
|
||||
char* software;
|
||||
char* version;
|
||||
} nostr_relay_basic_info_t;
|
||||
|
||||
typedef struct {
|
||||
int max_message_length;
|
||||
int max_subscriptions;
|
||||
int max_filters;
|
||||
int max_limit;
|
||||
int max_subid_length;
|
||||
int min_prefix;
|
||||
int max_event_tags;
|
||||
int max_content_length;
|
||||
int min_pow_difficulty;
|
||||
int auth_required;
|
||||
int payment_required;
|
||||
long created_at_lower_limit;
|
||||
long created_at_upper_limit;
|
||||
int restricted_writes;
|
||||
} nostr_relay_limitations_t;
|
||||
|
||||
typedef struct {
|
||||
char** relay_countries;
|
||||
size_t relay_countries_count;
|
||||
} nostr_relay_content_limitations_t;
|
||||
|
||||
typedef struct {
|
||||
char** language_tags;
|
||||
size_t language_tags_count;
|
||||
char** tags;
|
||||
size_t tags_count;
|
||||
char* posting_policy;
|
||||
} nostr_relay_community_preferences_t;
|
||||
|
||||
typedef struct {
|
||||
char* icon;
|
||||
} nostr_relay_icon_t;
|
||||
|
||||
typedef struct {
|
||||
// Basic information (always present)
|
||||
nostr_relay_basic_info_t basic;
|
||||
|
||||
// Optional sections (check has_* flags)
|
||||
int has_limitations;
|
||||
nostr_relay_limitations_t limitations;
|
||||
|
||||
int has_content_limitations;
|
||||
nostr_relay_content_limitations_t content_limitations;
|
||||
|
||||
int has_community_preferences;
|
||||
nostr_relay_community_preferences_t community_preferences;
|
||||
|
||||
int has_icon;
|
||||
nostr_relay_icon_t icon;
|
||||
} nostr_relay_info_t;
|
||||
|
||||
/**
|
||||
* Fetch relay information document from a relay URL
|
||||
* Converts WebSocket URLs to HTTP and retrieves NIP-11 document
|
||||
*
|
||||
* @param relay_url Relay URL (ws://, wss://, http://, or https://)
|
||||
* @param info_out OUTPUT: Pointer to relay info structure (caller must free with nostr_nip11_relay_info_free)
|
||||
* @param timeout_seconds HTTP timeout in seconds (0 for default 10 seconds)
|
||||
* @return NOSTR_SUCCESS if retrieved, NOSTR_ERROR_* on failure
|
||||
*/
|
||||
int nostr_nip11_fetch_relay_info(const char* relay_url, nostr_relay_info_t** info_out, int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Free relay information structure
|
||||
*
|
||||
* @param info Relay info structure to free (safe to pass NULL)
|
||||
*/
|
||||
void nostr_nip11_relay_info_free(nostr_relay_info_t* info);
|
||||
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// NIP-04: ENCRYPTED DIRECT MESSAGES
|
||||
|
||||
Reference in New Issue
Block a user