Compare commits

..
8 Commits
40 changed files with 6524 additions and 396 deletions
+61
View File
@@ -0,0 +1,61 @@
cmake_minimum_required(VERSION 3.16)
if(ESP_PLATFORM)
idf_component_register(
SRCS
"nostr_core/nostr_common.c"
"nostr_core/nostr_log.c"
"nostr_core/nip001.c"
"nostr_core/nip004.c"
"nostr_core/nip006.c"
"nostr_core/nip019.c"
"nostr_core/utils.c"
"nostr_core/crypto/nostr_secp256k1.c"
"nostr_core/crypto/nostr_aes.c"
"nostr_core/crypto/nostr_chacha20.c"
"cjson/cJSON.c"
"platform/esp32/nostr_platform_esp32.c"
"platform/esp32/nostr_http_esp32.c"
"platform/esp32/nostr_websocket_esp32.c"
INCLUDE_DIRS
"."
"nostr_core"
"nostr_core/crypto"
"cjson"
"nostr_websocket"
REQUIRES
secp256k1
esp_hw_support
esp_http_client
esp-tls
tcp_transport
mbedtls
)
target_compile_definitions(${COMPONENT_LIB} PRIVATE NOSTR_NO_FILESYSTEM=1)
else()
project(nostr_core_lib C)
add_library(nostr_core STATIC
nostr_core/nostr_common.c
nostr_core/nostr_log.c
nostr_core/request_validator.c
nostr_core/nip001.c
nostr_core/nip006.c
nostr_core/nip019.c
nostr_core/utils.c
nostr_core/crypto/nostr_secp256k1.c
nostr_core/crypto/nostr_aes.c
nostr_core/crypto/nostr_chacha20.c
cjson/cJSON.c
platform/linux.c
)
target_include_directories(nostr_core PUBLIC
.
nostr_core
nostr_core/crypto
cjson
nostr_websocket
)
endif()
+29 -13
View File
@@ -2,7 +2,7 @@
A C library for NOSTR protocol implementation. Work in progress.
[![Version](https://img.shields.io/badge/version-0.4.8-blue.svg)](VERSION)
[![Version](https://img.shields.io/badge/version-0.6.0-blue.svg)](VERSION)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](#license)
[![Build Status](https://img.shields.io/badge/build-passing-brightgreen.svg)](#building)
@@ -12,11 +12,11 @@ A C library for NOSTR protocol implementation. Work in progress.
### Core Protocol NIPs
- [x] [NIP-01](nips/01.md) - Basic protocol flow - event creation, signing, and validation
- [ ] [NIP-02](nips/02.md) - Contact List and Petnames
- [ ] [NIP-03](nips/03.md) - OpenTimestamps Attestations for Events
- [x] [NIP-03](nips/03.md) - OpenTimestamps Attestations for Events
- [x] [NIP-04](nips/04.md) - Encrypted Direct Messages (legacy)
- [x] [NIP-05](nips/05.md) - Mapping Nostr keys to DNS-based internet identifiers
- [x] [NIP-06](nips/06.md) - Basic key derivation from mnemonic seed phrase
- [ ] [NIP-07](nips/07.md) - `window.nostr` capability for web browsers
- [-] [NIP-07](nips/07.md) - `window.nostr` capability for web browsers
- [ ] [NIP-08](nips/08.md) - Handling Mentions
- [ ] [NIP-09](nips/09.md) - Event Deletion
- [ ] [NIP-10](nips/10.md) - Conventions for clients' use of `e` and `p` tags in text events
@@ -62,7 +62,7 @@ A C library for NOSTR protocol implementation. Work in progress.
- [ ] [NIP-52](nips/52.md) - Calendar Events
- [ ] [NIP-53](nips/53.md) - Live Activities
- [ ] [NIP-54](nips/54.md) - Wiki
- [ ] [NIP-55](nips/55.md) - Android Signer Application
- [-] [NIP-55](nips/55.md) - Android Signer Application
- [ ] [NIP-56](nips/56.md) - Reporting
- [ ] [NIP-57](nips/57.md) - Lightning Zaps
- [ ] [NIP-58](nips/58.md) - Badges
@@ -70,10 +70,10 @@ A C library for NOSTR protocol implementation. Work in progress.
- [ ] [NIP-60](nips/60.md) - Cashu Wallet
- [ ] [NIP-61](nips/61.md) - Nutzaps
- [ ] [NIP-62](nips/62.md) - Log events
- [ ] [NIP-64](nips/64.md) - Chess (PGN)
- [-] [NIP-64](nips/64.md) - Chess (PGN)
- [ ] [NIP-65](nips/65.md) - Relay List Metadata
- [ ] [NIP-66](nips/66.md) - Relay Monitor
- [ ] [NIP-68](nips/68.md) - Web badges
- [-] [NIP-68](nips/68.md) - Web badges
- [ ] [NIP-69](nips/69.md) - Peer-to-peer Order events
- [ ] [NIP-70](nips/70.md) - Protected Events
- [ ] [NIP-71](nips/71.md) - Video Events
@@ -85,7 +85,7 @@ A C library for NOSTR protocol implementation. Work in progress.
- [ ] [NIP-84](nips/84.md) - Highlights
- [ ] [NIP-86](nips/86.md) - Relay Management API
- [ ] [NIP-87](nips/87.md) - Relay List Recommendations
- [ ] [NIP-88](nips/88.md) - Stella: A Stellar Relay
- [-] [NIP-88](nips/88.md) - Stella: A Stellar Relay
- [ ] [NIP-89](nips/89.md) - Recommended Application Handlers
- [ ] [NIP-90](nips/90.md) - Data Vending Machines
- [ ] [NIP-92](nips/92.md) - Media Attachments
@@ -94,7 +94,7 @@ A C library for NOSTR protocol implementation. Work in progress.
- [ ] [NIP-98](nips/98.md) - HTTP Auth
- [ ] [NIP-99](nips/99.md) - Classified Listings
**Legend:** ✅ Fully Implemented | ⚠️ Partial Implementation | ❌ Not Implemented
**Legend:** ✅ Fully Implemented | ⚠️ Partial Implementation | ❌ Not Implemented | Not Applicable
**Implementation Summary:** 13 of 96+ NIPs fully implemented (13.5%)
@@ -508,6 +508,20 @@ int main(void) {
- **Windows** (MinGW, MSYS2)
- **Embedded Systems** (resource-constrained environments)
## 🔌 Embedded (ESP32) Support
`nostr_core_lib` now includes a platform abstraction layer and ESP32-native implementations for core networking and entropy APIs, while preserving desktop compatibility.
Implemented embedded capabilities include:
- platform random source abstraction via `nostr_platform_random()`
- ESP32 random provider using `esp_fill_random`
- ESP32 HTTP client implementation via `esp_http_client`
- ESP32 WebSocket/WSS transport via `esp_transport_ws` + TLS certificate bundle
- NIP-04 encrypted DM flow validated on-device against public relays
Reference ESP-IDF example project (in this workspace):
- `../esp32_send_kind4/` — Wi-Fi connect + `wss://` relay connect + send two kind-4 encrypted events + print relay responses
## 📄 Documentation
- **[POOL_API.md](POOL_API.md)** - Relay pool API notes
@@ -527,20 +541,22 @@ int main(void) {
## 📈 Version History
Current version: **0.4.8**
Current version: **0.6.0**
The library uses automatic semantic versioning based on Git tags. Each build increments the patch version automatically.
**Recent Developments:**
- **OpenSSL Migration**: Transitioned from mbedTLS to OpenSSL for improved compatibility
- **ESP32 Embedded Enablement**: Added PAL-based embedded support directly in `nostr_core_lib`
- **ESP32 HTTP + WSS**: Added ESP-IDF-backed HTTP and secure websocket transport implementations
- **NIP-04 On-Device Validation**: Verified encrypted kind-4 DM publish flow from ESP32 to live relays
- **NIP-05 Support**: DNS-based internet identifier verification
- **NIP-11 Support**: Relay information document fetching and parsing
- **NIP-19 Support**: Bech32-encoded entities (nsec/npub)
- **Enhanced WebSocket**: OpenSSL-based TLS WebSocket communication
- **Comprehensive Testing**: Extensive test suite and error handling
**Version Timeline:**
- `v0.4.x` - Current development releases with relay pool and expanded NIP support
- `v0.6.x` - Embedded-capable releases with ESP32 PAL, HTTP, and WSS support
- `v0.4.x` - Development releases with relay pool and expanded NIP support
- `v0.2.x` - Earlier OpenSSL-based development releases
- `v0.1.x` - Initial development releases
- Focus on core protocol implementation and OpenSSL-based crypto
@@ -580,7 +596,7 @@ gcc your_code.c ./libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcu
### Getting Help
- Check the `examples/` directory for working code
- Run `./build.sh test` to verify your environment
- Run `./build.sh -t` to verify your environment
- Review the comprehensive API documentation in `nostr_core/nostr_core.h`
## 📜 License
+1 -1
View File
@@ -1 +1 @@
0.5.4
0.6.2
+11 -3
View File
@@ -135,6 +135,7 @@ if [ "$HELP" = true ]; then
echo ""
echo "Available NIPs:"
echo " 001 - Basic Protocol (event creation, signing)"
echo " 003 - OpenTimestamps"
echo " 004 - Encryption (legacy)"
echo " 005 - DNS-based identifiers"
echo " 006 - Key derivation from mnemonic"
@@ -197,7 +198,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 017 019 042 044 046 059 060 061"
NEEDED_NIPS="001 003 004 005 006 011 013 017 019 021 042 044 046 059 060 061"
print_info "Forced: Building all available NIPs"
else
# Convert comma-separated list to space-separated with 3-digit format
@@ -216,7 +217,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 042 044 046 059 060 061"
NEEDED_NIPS="001 003 004 005 006 011 013 019 021 042 044 046 059 060 061"
elif [ -n "$DETECTED" ]; then
NEEDED_NIPS="$DETECTED"
print_success "Auto-detected NIPs: $(echo $NEEDED_NIPS | tr ' ' ',')"
@@ -234,7 +235,7 @@ fi
# If building tests or examples, include all NIPs to ensure compatibility
if ([ "$BUILD_TESTS" = true ] || [ "$BUILD_EXAMPLES" = true ]) && [ -z "$FORCE_NIPS" ]; then
NEEDED_NIPS="001 004 005 006 011 013 017 019 021 042 044 046 059 060 061"
NEEDED_NIPS="001 003 004 005 006 011 013 017 019 021 042 044 046 059 060 061"
print_info "Building tests/examples - including all available NIPs for compatibility"
fi
@@ -499,6 +500,8 @@ detect_system_curl
SOURCES="nostr_core/crypto/nostr_secp256k1.c"
SOURCES="$SOURCES nostr_core/crypto/nostr_aes.c"
SOURCES="$SOURCES nostr_core/crypto/nostr_chacha20.c"
SOURCES="$SOURCES nostr_core/crypto/nostr_poly1305.c"
SOURCES="$SOURCES nostr_core/crypto/nostr_chacha20poly1305.c"
SOURCES="$SOURCES cjson/cJSON.c"
SOURCES="$SOURCES nostr_core/utils.c"
SOURCES="$SOURCES nostr_core/nostr_common.c"
@@ -507,6 +510,8 @@ SOURCES="$SOURCES nostr_core/core_relay_pool.c"
SOURCES="$SOURCES nostr_core/nostr_log.c"
SOURCES="$SOURCES nostr_websocket/nostr_websocket_openssl.c"
SOURCES="$SOURCES nostr_core/request_validator.c"
SOURCES="$SOURCES nostr_core/nostr_http.c"
SOURCES="$SOURCES platform/linux.c"
NIP_DESCRIPTIONS=""
@@ -516,6 +521,7 @@ for nip in $NEEDED_NIPS; do
SOURCES="$SOURCES $NIP_FILE"
case $nip in
001) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-001(Basic)" ;;
003) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-003(OpenTimestamps)" ;;
004) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-004(Encrypt)" ;;
005) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-005(DNS)" ;;
006) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-006(Keys)" ;;
@@ -540,6 +546,8 @@ if echo "$NEEDED_NIPS" | grep -Eq '(^| )060( |$)|(^| )061( |$)'; then
SOURCES="$SOURCES nostr_core/cashu_mint.c"
fi
SOURCES="$SOURCES nostr_core/blossom_client.c"
# Build flags
CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"
CFLAGS="$CFLAGS -DENABLE_FILE_LOGGING -DENABLE_WEBSOCKET_LOGGING -DENABLE_DEBUG_LOGGING"
+46 -14
View File
@@ -1,7 +1,9 @@
#!/bin/bash
# increment_and_push.sh - Version increment and git automation script
# Usage: ./increment_and_push.sh "meaningful git comment"
# Usage:
# ./increment_and_push.sh "meaningful git comment"
# ./increment_and_push.sh --set-version vX.Y.Z "meaningful git comment"
set -e # Exit on error
@@ -65,16 +67,30 @@ if ! git rev-parse --git-dir > /dev/null 2>&1; then
exit 1
fi
# Check if we have a commit message
if [ $# -eq 0 ]; then
print_error "Usage: $0 \"meaningful git comment\""
echo ""
echo "Example: $0 \"Add enhanced subscription functionality\""
echo ""
exit 1
fi
TARGET_VERSION=""
COMMIT_MESSAGE=""
COMMIT_MESSAGE="$1"
# Parse arguments
if [ "$1" = "--set-version" ]; then
if [ $# -lt 3 ]; then
print_error "Usage: $0 --set-version vX.Y.Z \"meaningful git comment\""
echo ""
echo "Example: $0 --set-version v0.6.0 \"Release v0.6.0 with ESP32 embedded support\""
echo ""
exit 1
fi
TARGET_VERSION="$2"
COMMIT_MESSAGE="$3"
else
if [ $# -eq 0 ]; then
print_error "Usage: $0 \"meaningful git comment\""
echo ""
echo "Example: $0 \"Add enhanced subscription functionality\""
echo ""
exit 1
fi
COMMIT_MESSAGE="$1"
fi
# Check if nostr_core.h exists
if [ ! -f "nostr_core/nostr_core.h" ]; then
@@ -103,21 +119,37 @@ fi
print_info "Current version: $CURRENT_VERSION (Major: $VERSION_MAJOR, Minor: $VERSION_MINOR, Patch: $VERSION_PATCH)"
# Increment patch version
NEW_PATCH=$((VERSION_PATCH + 1))
NEW_VERSION="v$VERSION_MAJOR.$VERSION_MINOR.$NEW_PATCH"
if [ -n "$TARGET_VERSION" ]; then
if [[ ! "$TARGET_VERSION" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
print_error "Invalid target version format: $TARGET_VERSION (expected vX.Y.Z)"
exit 1
fi
NEW_VERSION="$TARGET_VERSION"
NEW_MAJOR="${BASH_REMATCH[1]}"
NEW_MINOR="${BASH_REMATCH[2]}"
NEW_PATCH="${BASH_REMATCH[3]}"
else
# Increment patch version
NEW_MAJOR="$VERSION_MAJOR"
NEW_MINOR="$VERSION_MINOR"
NEW_PATCH=$((VERSION_PATCH + 1))
NEW_VERSION="v$NEW_MAJOR.$NEW_MINOR.$NEW_PATCH"
fi
print_info "New version will be: $NEW_VERSION"
# Update version in nostr_core.h
sed -i "s/#define VERSION .*/#define VERSION \"$NEW_VERSION\"/" nostr_core/nostr_core.h
sed -i "s/#define VERSION_MAJOR .*/#define VERSION_MAJOR $NEW_MAJOR/" nostr_core/nostr_core.h
sed -i "s/#define VERSION_MINOR .*/#define VERSION_MINOR $NEW_MINOR/" nostr_core/nostr_core.h
sed -i "s/#define VERSION_PATCH .*/#define VERSION_PATCH $NEW_PATCH/" nostr_core/nostr_core.h
print_success "Updated version in nostr_core.h"
# Check if VERSION file exists and update it
if [ -f "VERSION" ]; then
echo "$VERSION_MAJOR.$VERSION_MINOR.$NEW_PATCH" > VERSION
echo "$NEW_MAJOR.$NEW_MINOR.$NEW_PATCH" > VERSION
print_success "Updated VERSION file"
fi
+513
View File
@@ -0,0 +1,513 @@
/*
* Blossom HTTP Client
*/
#include "blossom_client.h"
#include "../cjson/cJSON.h"
#include "nip001.h"
#include "nostr_http.h"
#include "utils.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
static int is_hex64_local(const char* s) {
if (!s || strlen(s) != 64) return 0;
for (int i = 0; i < 64; i++) {
char c = s[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
return 0;
}
}
return 1;
}
static void trim_trailing_slash_local(const char* in, char* out, size_t out_sz) {
if (!in || !out || out_sz == 0) return;
snprintf(out, out_sz, "%s", in);
size_t n = strlen(out);
while (n > 0 && out[n - 1] == '/') {
out[n - 1] = '\0';
n--;
}
}
static int build_blob_url_local(const char* server_url, const char* sha256_hex, char* out, size_t out_sz) {
if (!server_url || !sha256_hex || !out || out_sz == 0) return -1;
char base[512];
trim_trailing_slash_local(server_url, base, sizeof(base));
int n = snprintf(out, out_sz, "%s/%s", base, sha256_hex);
if (n < 0 || (size_t)n >= out_sz) return -1;
return 0;
}
static int parse_descriptor_local(const char* json_text, blossom_blob_descriptor_t* out) {
if (!json_text || !out) return NOSTR_ERROR_INVALID_INPUT;
cJSON* root = cJSON_Parse(json_text);
if (!root) return NOSTR_ERROR_IO_FAILED;
cJSON* sha = cJSON_GetObjectItemCaseSensitive(root, "sha256");
if (!sha) sha = cJSON_GetObjectItemCaseSensitive(root, "x");
cJSON* url = cJSON_GetObjectItemCaseSensitive(root, "url");
cJSON* size = cJSON_GetObjectItemCaseSensitive(root, "size");
cJSON* ct = cJSON_GetObjectItemCaseSensitive(root, "content_type");
if (!ct) ct = cJSON_GetObjectItemCaseSensitive(root, "m");
cJSON* created = cJSON_GetObjectItemCaseSensitive(root, "created");
if (!created) created = cJSON_GetObjectItemCaseSensitive(root, "created_at");
memset(out, 0, sizeof(*out));
if (sha && cJSON_IsString(sha) && sha->valuestring) {
snprintf(out->sha256, sizeof(out->sha256), "%s", sha->valuestring);
}
if (url && cJSON_IsString(url) && url->valuestring) {
snprintf(out->url, sizeof(out->url), "%s", url->valuestring);
}
if (size && cJSON_IsNumber(size)) out->size = (long)cJSON_GetNumberValue(size);
if (ct && cJSON_IsString(ct) && ct->valuestring) {
snprintf(out->content_type, sizeof(out->content_type), "%s", ct->valuestring);
}
if (created && cJSON_IsNumber(created)) out->created = (long)cJSON_GetNumberValue(created);
cJSON_Delete(root);
return NOSTR_SUCCESS;
}
void blossom_set_ca_bundle(const char* ca_bundle_path) {
nostr_http_set_ca_bundle(ca_bundle_path);
}
char* blossom_create_auth_header(const unsigned char* private_key,
const char* operation,
const char* sha256_hex,
int expiration_seconds) {
if (!private_key || !operation || operation[0] == '\0') return NULL;
if (sha256_hex && !is_hex64_local(sha256_hex)) return NULL;
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
cJSON* t_tag = cJSON_CreateArray();
cJSON_AddItemToArray(t_tag, cJSON_CreateString("t"));
cJSON_AddItemToArray(t_tag, cJSON_CreateString(operation));
cJSON_AddItemToArray(tags, t_tag);
if (sha256_hex) {
cJSON* x_tag = cJSON_CreateArray();
cJSON_AddItemToArray(x_tag, cJSON_CreateString("x"));
cJSON_AddItemToArray(x_tag, cJSON_CreateString(sha256_hex));
cJSON_AddItemToArray(tags, x_tag);
}
int exp = expiration_seconds > 0 ? expiration_seconds : 300;
long until = (long)time(NULL) + exp;
char exp_buf[32];
snprintf(exp_buf, sizeof(exp_buf), "%ld", until);
cJSON* e_tag = cJSON_CreateArray();
cJSON_AddItemToArray(e_tag, cJSON_CreateString("expiration"));
cJSON_AddItemToArray(e_tag, cJSON_CreateString(exp_buf));
cJSON_AddItemToArray(tags, e_tag);
cJSON* evt = nostr_create_and_sign_event(24242, "", tags, private_key, time(NULL));
cJSON_Delete(tags);
if (!evt) return NULL;
char* evt_json = cJSON_PrintUnformatted(evt);
cJSON_Delete(evt);
if (!evt_json) return NULL;
size_t evt_len = strlen(evt_json);
size_t b64_cap = ((evt_len + 2U) / 3U) * 4U + 8U;
char* b64 = (char*)malloc(b64_cap);
if (!b64) {
free(evt_json);
return NULL;
}
size_t n = base64_encode((const unsigned char*)evt_json, evt_len, b64, b64_cap);
free(evt_json);
if (n == 0) {
free(b64);
return NULL;
}
size_t hdr_cap = n + 16U;
char* header = (char*)malloc(hdr_cap);
if (!header) {
free(b64);
return NULL;
}
snprintf(header, hdr_cap, "Nostr %s", b64);
free(b64);
return header;
}
int blossom_upload(const char* server_url,
const unsigned char* data,
size_t data_len,
const char* content_type,
const unsigned char* private_key,
const char* sha256_hex,
int timeout_seconds,
blossom_blob_descriptor_t* descriptor_out) {
if (!server_url || !data || data_len == 0 || !descriptor_out) return NOSTR_ERROR_INVALID_INPUT;
char hash_hex[65] = {0};
if (sha256_hex) {
if (!is_hex64_local(sha256_hex)) return NOSTR_ERROR_INVALID_INPUT;
snprintf(hash_hex, sizeof(hash_hex), "%s", sha256_hex);
} else {
unsigned char hash[32];
if (nostr_sha256(data, data_len, hash) != 0) return NOSTR_ERROR_CRYPTO_FAILED;
nostr_bytes_to_hex(hash, 32, hash_hex);
}
char url[1024];
char base[512];
trim_trailing_slash_local(server_url, base, sizeof(base));
snprintf(url, sizeof(url), "%s/upload", base);
char auth_buf[1024] = {0};
char ctype_buf[256] = {0};
const char* headers[4] = {"Accept: application/json", NULL, NULL, NULL};
int h = 1;
if (content_type && content_type[0] != '\0') {
snprintf(ctype_buf, sizeof(ctype_buf), "Content-Type: %s", content_type);
headers[h++] = ctype_buf;
}
char* auth = NULL;
if (private_key) {
auth = blossom_create_auth_header(private_key, "upload", hash_hex, 300);
if (auth) {
snprintf(auth_buf, sizeof(auth_buf), "Authorization: %s", auth);
headers[h++] = auth_buf;
}
}
headers[h] = NULL;
nostr_http_request_t req;
memset(&req, 0, sizeof(req));
req.method = "PUT";
req.url = url;
req.headers = headers;
req.body = data;
req.body_len = data_len;
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 20;
req.follow_redirects = 1;
req.max_redirects = 3;
nostr_http_response_t resp;
int rc = nostr_http_request(&req, &resp);
free(auth);
if (rc != NOSTR_SUCCESS) return rc;
if (resp.status_code < 200 || resp.status_code >= 300) {
nostr_http_response_free(&resp);
return NOSTR_ERROR_NETWORK_FAILED;
}
int prc = parse_descriptor_local(resp.body ? resp.body : "{}", descriptor_out);
if (prc != NOSTR_SUCCESS) {
memset(descriptor_out, 0, sizeof(*descriptor_out));
snprintf(descriptor_out->sha256, sizeof(descriptor_out->sha256), "%s", hash_hex);
descriptor_out->size = (long)data_len;
if (content_type && content_type[0] != '\0') {
snprintf(descriptor_out->content_type, sizeof(descriptor_out->content_type), "%s", content_type);
}
}
nostr_http_response_free(&resp);
return NOSTR_SUCCESS;
}
int blossom_upload_file(const char* server_url,
const char* file_path,
const char* content_type,
const unsigned char* private_key,
int timeout_seconds,
blossom_blob_descriptor_t* descriptor_out) {
if (!server_url || !file_path || !descriptor_out) return NOSTR_ERROR_INVALID_INPUT;
FILE* fp = fopen(file_path, "rb");
if (!fp) return NOSTR_ERROR_IO_FAILED;
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return NOSTR_ERROR_IO_FAILED;
}
long sz = ftell(fp);
if (sz < 0) {
fclose(fp);
return NOSTR_ERROR_IO_FAILED;
}
if (fseek(fp, 0, SEEK_SET) != 0) {
fclose(fp);
return NOSTR_ERROR_IO_FAILED;
}
unsigned char* buf = (unsigned char*)malloc((size_t)sz);
if (!buf) {
fclose(fp);
return NOSTR_ERROR_MEMORY_FAILED;
}
size_t n = fread(buf, 1, (size_t)sz, fp);
fclose(fp);
if (n != (size_t)sz) {
free(buf);
return NOSTR_ERROR_IO_FAILED;
}
int rc = blossom_upload(server_url, buf, n, content_type, private_key, NULL, timeout_seconds, descriptor_out);
free(buf);
return rc;
}
int blossom_download(const char* server_url,
const char* sha256_hex,
int timeout_seconds,
size_t max_bytes,
unsigned char** body_out,
size_t* body_len_out,
char* content_type_out,
size_t content_type_out_size) {
if (!server_url || !sha256_hex || !body_out || !body_len_out || !is_hex64_local(sha256_hex)) {
return NOSTR_ERROR_INVALID_INPUT;
}
*body_out = NULL;
*body_len_out = 0;
if (content_type_out && content_type_out_size > 0) content_type_out[0] = '\0';
char url[1024];
if (build_blob_url_local(server_url, sha256_hex, url, sizeof(url)) != 0) return NOSTR_ERROR_INVALID_INPUT;
nostr_http_request_t req;
memset(&req, 0, sizeof(req));
req.method = "GET";
req.url = url;
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 20;
req.max_response_bytes = max_bytes;
req.follow_redirects = 1;
req.max_redirects = 3;
nostr_http_response_t resp;
int rc = nostr_http_request(&req, &resp);
if (rc != NOSTR_SUCCESS) return rc;
if (resp.status_code < 200 || resp.status_code >= 300) {
nostr_http_response_free(&resp);
return NOSTR_ERROR_NETWORK_FAILED;
}
unsigned char* out = (unsigned char*)malloc(resp.body_len > 0 ? resp.body_len : 1);
if (!out) {
nostr_http_response_free(&resp);
return NOSTR_ERROR_MEMORY_FAILED;
}
if (resp.body_len > 0 && resp.body) memcpy(out, resp.body, resp.body_len);
*body_out = out;
*body_len_out = resp.body_len;
if (content_type_out && content_type_out_size > 0 && resp.content_type && resp.content_type[0] != '\0') {
snprintf(content_type_out, content_type_out_size, "%s", resp.content_type);
}
nostr_http_response_free(&resp);
return NOSTR_SUCCESS;
}
int blossom_download_to_file(const char* server_url,
const char* sha256_hex,
const char* output_path,
int timeout_seconds,
size_t max_bytes,
blossom_blob_descriptor_t* descriptor_out) {
if (!server_url || !sha256_hex || !output_path || !descriptor_out) return NOSTR_ERROR_INVALID_INPUT;
unsigned char* body = NULL;
size_t body_len = 0;
char content_type[128] = {0};
int rc = blossom_download(server_url, sha256_hex, timeout_seconds, max_bytes, &body, &body_len, content_type, sizeof(content_type));
if (rc != NOSTR_SUCCESS) return rc;
FILE* fp = fopen(output_path, "wb");
if (!fp) {
free(body);
return NOSTR_ERROR_IO_FAILED;
}
size_t wn = fwrite(body, 1, body_len, fp);
fclose(fp);
if (wn != body_len) {
free(body);
return NOSTR_ERROR_IO_FAILED;
}
unsigned char hash[32];
if (nostr_sha256(body, body_len, hash) != 0) {
free(body);
return NOSTR_ERROR_CRYPTO_FAILED;
}
free(body);
char hash_hex[65];
nostr_bytes_to_hex(hash, 32, hash_hex);
memset(descriptor_out, 0, sizeof(*descriptor_out));
snprintf(descriptor_out->sha256, sizeof(descriptor_out->sha256), "%s", hash_hex);
descriptor_out->size = (long)body_len;
if (content_type[0] != '\0') snprintf(descriptor_out->content_type, sizeof(descriptor_out->content_type), "%s", content_type);
build_blob_url_local(server_url, sha256_hex, descriptor_out->url, sizeof(descriptor_out->url));
return NOSTR_SUCCESS;
}
int blossom_head(const char* server_url,
const char* sha256_hex,
int timeout_seconds,
blossom_blob_descriptor_t* descriptor_out) {
if (!server_url || !sha256_hex || !descriptor_out || !is_hex64_local(sha256_hex)) {
return NOSTR_ERROR_INVALID_INPUT;
}
char url[1024];
if (build_blob_url_local(server_url, sha256_hex, url, sizeof(url)) != 0) return NOSTR_ERROR_INVALID_INPUT;
nostr_http_request_t req;
memset(&req, 0, sizeof(req));
req.method = "HEAD";
req.url = url;
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 10;
req.capture_headers = 1;
nostr_http_response_t resp;
int rc = nostr_http_request(&req, &resp);
if (rc != NOSTR_SUCCESS) return rc;
if (resp.status_code < 200 || resp.status_code >= 300) {
nostr_http_response_free(&resp);
return NOSTR_ERROR_NETWORK_FAILED;
}
memset(descriptor_out, 0, sizeof(*descriptor_out));
snprintf(descriptor_out->sha256, sizeof(descriptor_out->sha256), "%s", sha256_hex);
if (build_blob_url_local(server_url, sha256_hex, descriptor_out->url, sizeof(descriptor_out->url)) != 0) {
nostr_http_response_free(&resp);
return NOSTR_ERROR_INVALID_INPUT;
}
if (resp.content_type && resp.content_type[0] != '\0') {
snprintf(descriptor_out->content_type, sizeof(descriptor_out->content_type), "%s", resp.content_type);
}
if (resp.headers_raw) {
const char* key = "Content-Length:";
char* p = strstr(resp.headers_raw, key);
if (p) {
p += strlen(key);
while (*p == ' ' || *p == '\t') p++;
descriptor_out->size = strtol(p, NULL, 10);
}
}
nostr_http_response_free(&resp);
return NOSTR_SUCCESS;
}
int blossom_delete(const char* server_url,
const char* sha256_hex,
const unsigned char* private_key,
int timeout_seconds) {
if (!server_url || !sha256_hex || !private_key || !is_hex64_local(sha256_hex)) {
return NOSTR_ERROR_INVALID_INPUT;
}
char url[1024];
if (build_blob_url_local(server_url, sha256_hex, url, sizeof(url)) != 0) return NOSTR_ERROR_INVALID_INPUT;
char* auth = blossom_create_auth_header(private_key, "delete", sha256_hex, 300);
if (!auth) return NOSTR_ERROR_CRYPTO_FAILED;
char auth_header[1200];
snprintf(auth_header, sizeof(auth_header), "Authorization: %s", auth);
free(auth);
const char* headers[] = {"Accept: application/json", auth_header, NULL};
nostr_http_request_t req;
memset(&req, 0, sizeof(req));
req.method = "DELETE";
req.url = url;
req.headers = headers;
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 20;
nostr_http_response_t resp;
int rc = nostr_http_request(&req, &resp);
if (rc != NOSTR_SUCCESS) return rc;
int ok = (resp.status_code >= 200 && resp.status_code < 300) ? NOSTR_SUCCESS : NOSTR_ERROR_NETWORK_FAILED;
nostr_http_response_free(&resp);
return ok;
}
int blossom_list(const char* server_url,
const char* pubkey_hex,
int timeout_seconds,
blossom_blob_descriptor_t** descriptors_out,
int* count_out) {
if (!server_url || !pubkey_hex || !descriptors_out || !count_out) return NOSTR_ERROR_INVALID_INPUT;
*descriptors_out = NULL;
*count_out = 0;
char base[512];
trim_trailing_slash_local(server_url, base, sizeof(base));
char url[1024];
snprintf(url, sizeof(url), "%s/list/%s", base, pubkey_hex);
nostr_http_request_t req;
memset(&req, 0, sizeof(req));
req.method = "GET";
req.url = url;
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 20;
nostr_http_response_t resp;
int rc = nostr_http_request(&req, &resp);
if (rc != NOSTR_SUCCESS) return rc;
if (resp.status_code < 200 || resp.status_code >= 300) {
nostr_http_response_free(&resp);
return NOSTR_ERROR_NETWORK_FAILED;
}
cJSON* arr = cJSON_Parse(resp.body ? resp.body : "[]");
if (!arr || !cJSON_IsArray(arr)) {
cJSON_Delete(arr);
nostr_http_response_free(&resp);
return NOSTR_ERROR_IO_FAILED;
}
int n = cJSON_GetArraySize(arr);
blossom_blob_descriptor_t* out = (blossom_blob_descriptor_t*)calloc((size_t)n, sizeof(blossom_blob_descriptor_t));
if (!out && n > 0) {
cJSON_Delete(arr);
nostr_http_response_free(&resp);
return NOSTR_ERROR_MEMORY_FAILED;
}
for (int i = 0; i < n; i++) {
cJSON* it = cJSON_GetArrayItem(arr, i);
char* tmp = cJSON_PrintUnformatted(it);
if (tmp) {
(void)parse_descriptor_local(tmp, &out[i]);
free(tmp);
}
}
*descriptors_out = out;
*count_out = n;
cJSON_Delete(arr);
nostr_http_response_free(&resp);
return NOSTR_SUCCESS;
}
+82
View File
@@ -0,0 +1,82 @@
/*
* Blossom HTTP Client
*/
#ifndef NOSTR_BLOSSOM_CLIENT_H
#define NOSTR_BLOSSOM_CLIENT_H
#include "nostr_common.h"
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
char sha256[65];
char url[512];
long size;
char content_type[128];
long created;
} blossom_blob_descriptor_t;
void blossom_set_ca_bundle(const char* ca_bundle_path);
char* blossom_create_auth_header(const unsigned char* private_key,
const char* operation,
const char* sha256_hex,
int expiration_seconds);
int blossom_upload(const char* server_url,
const unsigned char* data,
size_t data_len,
const char* content_type,
const unsigned char* private_key,
const char* sha256_hex,
int timeout_seconds,
blossom_blob_descriptor_t* descriptor_out);
int blossom_upload_file(const char* server_url,
const char* file_path,
const char* content_type,
const unsigned char* private_key,
int timeout_seconds,
blossom_blob_descriptor_t* descriptor_out);
int blossom_download(const char* server_url,
const char* sha256_hex,
int timeout_seconds,
size_t max_bytes,
unsigned char** body_out,
size_t* body_len_out,
char* content_type_out,
size_t content_type_out_size);
int blossom_download_to_file(const char* server_url,
const char* sha256_hex,
const char* output_path,
int timeout_seconds,
size_t max_bytes,
blossom_blob_descriptor_t* descriptor_out);
int blossom_head(const char* server_url,
const char* sha256_hex,
int timeout_seconds,
blossom_blob_descriptor_t* descriptor_out);
int blossom_delete(const char* server_url,
const char* sha256_hex,
const unsigned char* private_key,
int timeout_seconds);
int blossom_list(const char* server_url,
const char* pubkey_hex,
int timeout_seconds,
blossom_blob_descriptor_t** descriptors_out,
int* count_out);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_BLOSSOM_CLIENT_H */
+1226 -106
View File
File diff suppressed because it is too large Load Diff
+63 -1
View File
@@ -17,11 +17,28 @@ extern "C" {
#define CASHU_API_VERSION "v1"
typedef struct {
char id[17];
char id[NOSTR_CASHU_KEYSET_ID_HEX_SIZE];
char unit[16];
int active;
} cashu_keyset_t;
typedef enum {
CASHU_TOKEN_FORMAT_A = 0,
CASHU_TOKEN_FORMAT_B = 1
} cashu_token_format_t;
typedef struct {
uint64_t amount;
char pubkey[67];
} cashu_amount_key_t;
typedef struct {
char keyset_id[65];
char unit[16];
cashu_amount_key_t* keys;
int key_count;
} cashu_keyset_keys_t;
typedef struct {
char* name;
char* pubkey;
@@ -106,12 +123,38 @@ typedef struct {
int state_count;
} cashu_checkstate_response_t;
typedef struct {
char* mint_url;
nostr_cashu_proof_t* proofs;
int proof_count;
} cashu_decoded_token_t;
void cashu_mint_set_ca_bundle(const char* ca_bundle_path);
int cashu_mint_get_info(const char* mint_url,
cashu_mint_info_t* info_out,
int timeout_seconds);
void cashu_mint_free_info(cashu_mint_info_t* info);
int cashu_mint_get_keysets(const char* mint_url,
cashu_keyset_t** keysets_out,
int* keyset_count_out,
int timeout_seconds);
void cashu_mint_free_keysets(cashu_keyset_t* keysets);
int cashu_mint_get_keys(const char* mint_url,
const char* keyset_id,
cashu_keyset_keys_t* keys_out,
int timeout_seconds);
void cashu_mint_free_keyset_keys(cashu_keyset_keys_t* keys);
int cashu_keyset_keys_find_pubkey_for_amount(const cashu_keyset_keys_t* keys,
uint64_t amount,
const char** pubkey_out);
int cashu_mint_request_mint_quote(const char* mint_url,
uint64_t amount,
const char* unit,
@@ -203,6 +246,25 @@ int cashu_parse_checkstate_response(cJSON* response_body,
void cashu_mint_free_checkstate_response(cashu_checkstate_response_t* response);
int cashu_decode_token(const char* token_string,
cashu_decoded_token_t* token_out);
int cashu_encode_token(const cashu_decoded_token_t* token,
cashu_token_format_t format,
char** token_string_out);
void cashu_free_decoded_token(cashu_decoded_token_t* token);
int cashu_blind_message(const char* secret,
const char* mint_pubkey_hex,
char out_B_hex[67],
unsigned char out_r[32]);
int cashu_unblind_signature(const char* C_blinded_hex,
const char* mint_pubkey_hex,
const unsigned char r[32],
char out_C_hex[67]);
#ifdef __cplusplus
}
#endif
+135 -30
View File
@@ -178,6 +178,9 @@ struct nostr_pool_subscription {
// Per-relay timeout tracking
time_t* relay_last_activity; // Last activity time per relay
// Guard to ensure on_eose is emitted only once per subscription
int eose_callback_fired;
};
struct nostr_relay_pool {
@@ -507,16 +510,32 @@ static void attempt_reconnect(relay_connection_t* relay) {
relay->reconnect_attempts++;
if (ensure_relay_connection(relay) == 0) {
// Success! Reset reconnection state
relay->reconnect_attempts = 0;
relay->next_reconnect_time = 0;
// Success. Keep reconnect_attempts until the connection proves stable.
// This prevents rapid connect->disconnect flapping from immediately resetting backoff.
int reset_after_s = relay->pool->reconnect_config.reconnect_reset_stability_seconds;
if (reset_after_s <= 0) {
// Backward-compatible behavior: reset immediately when cooldown is disabled.
relay->reconnect_attempts = 0;
relay->next_reconnect_time = 0;
} else {
int delay_ms = calculate_reconnect_delay(relay);
int delay_s = (delay_ms + 999) / 1000;
if (delay_s < 1) {
delay_s = 1;
}
relay->next_reconnect_time = time(NULL) + delay_s;
}
// Restore subscriptions on reconnect
restore_subscriptions_on_reconnect(relay);
} else {
// Failed - schedule next attempt with backoff
int delay_ms = calculate_reconnect_delay(relay);
relay->next_reconnect_time = time(NULL) + (delay_ms / 1000);
int delay_s = (delay_ms + 999) / 1000;
if (delay_s < 1) {
delay_s = 1;
}
relay->next_reconnect_time = time(NULL) + delay_s;
}
}
@@ -615,6 +634,7 @@ nostr_pool_reconnect_config_t* nostr_pool_reconnect_config_default(void) {
.initial_reconnect_delay_ms = 1000,
.max_reconnect_delay_ms = 30000,
.reconnect_backoff_multiplier = 2,
.reconnect_reset_stability_seconds = 30,
.ping_interval_seconds = 30,
.pong_timeout_seconds = 10
};
@@ -957,9 +977,9 @@ int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription) {
if (!subscription || subscription->closed) {
return NOSTR_ERROR_INVALID_INPUT;
}
subscription->closed = 1;
// Send CLOSE messages to all relays
for (int i = 0; i < subscription->relay_count; i++) {
relay_connection_t* relay = find_relay_by_url(subscription->pool, subscription->relay_urls[i]);
@@ -967,7 +987,7 @@ int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription) {
nostr_relay_send_close(relay->ws_client, subscription->subscription_id);
}
}
// Remove from pool
nostr_relay_pool_t* pool = subscription->pool;
for (int i = 0; i < pool->subscription_count; i++) {
@@ -980,7 +1000,7 @@ int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription) {
break;
}
}
// Cleanup subscription
cJSON_Delete(subscription->filter);
for (int i = 0; i < subscription->relay_count; i++) {
@@ -988,11 +1008,55 @@ int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription) {
}
free(subscription->relay_urls);
free(subscription->eose_received);
free(subscription->relay_last_activity);
if (subscription->collected_events) {
for (int i = 0; i < subscription->collected_event_count; i++) {
cJSON_Delete(subscription->collected_events[i]);
}
free(subscription->collected_events);
}
free(subscription);
return NOSTR_SUCCESS;
}
static int maybe_complete_subscription_eose(nostr_pool_subscription_t* sub) {
if (!sub || sub->closed || sub->eose_callback_fired) {
return 0;
}
int all_eose = 1;
for (int j = 0; j < sub->relay_count; j++) {
if (!sub->eose_received[j]) {
all_eose = 0;
break;
}
}
if (!all_eose) {
return 0;
}
sub->eose_callback_fired = 1;
if (sub->on_eose) {
// FIRST mode: no events collected, pass NULL/0
if (sub->result_mode == NOSTR_POOL_EOSE_FIRST) {
sub->on_eose(NULL, 0, sub->user_data);
} else {
// FULL_SET or MOST_RECENT: pass collected events
sub->on_eose(sub->collected_events, sub->collected_event_count, sub->user_data);
}
}
if (sub->close_on_eose && !sub->closed) {
nostr_pool_subscription_close(sub);
return 1;
}
return 0;
}
// Event processing
static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t* relay, const char* message) {
if (!pool || !relay || !message) {
@@ -1032,6 +1096,15 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
}
if (sub) {
// Track per-relay activity for timeout fallback.
time_t now = time(NULL);
for (int j = 0; j < sub->relay_count; j++) {
if (strcmp(sub->relay_urls[j], relay->url) == 0) {
sub->relay_last_activity[j] = now;
break;
}
}
// Check for duplicate (per-subscription deduplication)
int is_duplicate = 0;
if (sub->enable_deduplication) {
@@ -1099,32 +1172,15 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
}
}
// Check if all relays have sent EOSE
int all_eose = 1;
// Keep activity updated on explicit EOSE from this relay
for (int j = 0; j < sub->relay_count; j++) {
if (!sub->eose_received[j]) {
all_eose = 0;
if (strcmp(sub->relay_urls[j], relay->url) == 0) {
sub->relay_last_activity[j] = time(NULL);
break;
}
}
if (all_eose) {
if (sub->on_eose) {
// Pass collected events based on result mode
if (sub->result_mode == NOSTR_POOL_EOSE_FIRST) {
// FIRST mode: no events collected, pass NULL/0
sub->on_eose(NULL, 0, sub->user_data);
} else {
// FULL_SET or MOST_RECENT: pass collected events
sub->on_eose(sub->collected_events, sub->collected_event_count, sub->user_data);
}
}
// Auto-close subscription if close_on_eose is enabled
if (sub->close_on_eose && !sub->closed) {
nostr_pool_subscription_close(sub);
}
}
(void)maybe_complete_subscription_eose(sub);
break;
}
}
@@ -1994,6 +2050,21 @@ int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms) {
relay->status = NOSTR_POOL_RELAY_CONNECTED;
// If the relay remains connected long enough, clear reconnect backoff.
if (relay->reconnect_attempts > 0) {
int reset_after_s = pool->reconnect_config.reconnect_reset_stability_seconds;
if (reset_after_s <= 0) {
relay->reconnect_attempts = 0;
relay->next_reconnect_time = 0;
} else if (relay->connect_time > 0) {
time_t now = time(NULL);
if (now - relay->connect_time >= reset_after_s) {
relay->reconnect_attempts = 0;
relay->next_reconnect_time = 0;
}
}
}
// Connection health monitoring (ping/pong)
check_connection_health(relay);
@@ -2018,5 +2089,39 @@ int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms) {
}
}
// Timeout fallback for relays that never emit EOSE:
// if no activity from a relay for relay_timeout_seconds, treat it as done.
time_t now = time(NULL);
for (int i = 0; i < pool->subscription_count; i++) {
nostr_pool_subscription_t* sub = pool->subscriptions[i];
if (!sub || sub->closed || sub->eose_callback_fired) {
continue;
}
if (sub->relay_timeout_seconds <= 0) {
continue;
}
int changed = 0;
for (int j = 0; j < sub->relay_count; j++) {
if (sub->eose_received[j]) {
continue;
}
if (sub->relay_last_activity &&
now - sub->relay_last_activity[j] >= sub->relay_timeout_seconds) {
sub->eose_received[j] = 1;
changed = 1;
}
}
if (changed) {
if (maybe_complete_subscription_eose(sub)) {
// Subscription list may have shifted due to auto-close.
i--;
}
}
}
return events_processed;
}
+192
View File
@@ -0,0 +1,192 @@
/*
* nostr_chacha20poly1305.c - ChaCha20-Poly1305 AEAD implementation
*
* RFC 8439 Section 2.8
*/
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
// ChaCha20 private API (provided by nostr_chacha20.c)
int chacha20_block(const uint8_t key[32], uint32_t counter,
const uint8_t nonce[12], uint8_t output[64]);
int chacha20_encrypt(const uint8_t key[32], uint32_t counter,
const uint8_t nonce[12], const uint8_t* input,
uint8_t* output, size_t length);
// Poly1305 API (provided by nostr_poly1305.c)
int nostr_poly1305_mac(const unsigned char key[32], const unsigned char *msg,
size_t msg_len, unsigned char tag[16]);
static void store64_le(unsigned char out[8], uint64_t v) {
out[0] = (unsigned char)(v & 0xff);
out[1] = (unsigned char)((v >> 8) & 0xff);
out[2] = (unsigned char)((v >> 16) & 0xff);
out[3] = (unsigned char)((v >> 24) & 0xff);
out[4] = (unsigned char)((v >> 32) & 0xff);
out[5] = (unsigned char)((v >> 40) & 0xff);
out[6] = (unsigned char)((v >> 48) & 0xff);
out[7] = (unsigned char)((v >> 56) & 0xff);
}
static int constant_time_tag_eq(const unsigned char a[16], const unsigned char b[16]) {
unsigned char diff = 0;
size_t i;
for (i = 0; i < 16; i++) {
diff |= (unsigned char)(a[i] ^ b[i]);
}
return diff == 0;
}
static int build_poly1305_input(const unsigned char *aad, size_t aad_len,
const unsigned char *ciphertext, size_t ct_len,
unsigned char **out, size_t *out_len) {
size_t aad_pad = (16 - (aad_len % 16)) % 16;
size_t ct_pad = (16 - (ct_len % 16)) % 16;
size_t total = aad_len + aad_pad + ct_len + ct_pad + 16; // len(aad)||len(ct)
unsigned char *buf;
size_t pos = 0;
unsigned char len_block[16];
if (!out || !out_len) {
return -1;
}
buf = (unsigned char *)malloc(total);
if (!buf) {
return -1;
}
if (aad_len > 0 && aad) {
memcpy(buf + pos, aad, aad_len);
pos += aad_len;
}
if (aad_pad > 0) {
memset(buf + pos, 0, aad_pad);
pos += aad_pad;
}
if (ct_len > 0 && ciphertext) {
memcpy(buf + pos, ciphertext, ct_len);
pos += ct_len;
}
if (ct_pad > 0) {
memset(buf + pos, 0, ct_pad);
pos += ct_pad;
}
store64_le(len_block, (uint64_t)aad_len);
store64_le(len_block + 8, (uint64_t)ct_len);
memcpy(buf + pos, len_block, sizeof(len_block));
pos += sizeof(len_block);
*out = buf;
*out_len = pos;
return 0;
}
int nostr_chacha20poly1305_encrypt(const unsigned char key[32],
const unsigned char nonce[12],
const unsigned char *aad, size_t aad_len,
const unsigned char *plaintext, size_t pt_len,
unsigned char *ciphertext,
unsigned char tag[16]) {
unsigned char block0[64];
unsigned char poly_key[32];
unsigned char *poly_in = NULL;
size_t poly_in_len = 0;
int rc;
if (!key || !nonce || !ciphertext || !tag || (!plaintext && pt_len != 0) || (!aad && aad_len != 0)) {
return -1;
}
if (chacha20_block(key, 0, nonce, block0) != 0) {
return -1;
}
memcpy(poly_key, block0, sizeof(poly_key));
if (pt_len > 0) {
rc = chacha20_encrypt(key, 1, nonce, plaintext, ciphertext, pt_len);
if (rc != 0) {
memset(poly_key, 0, sizeof(poly_key));
return -1;
}
}
rc = build_poly1305_input(aad, aad_len, ciphertext, pt_len, &poly_in, &poly_in_len);
if (rc != 0) {
memset(poly_key, 0, sizeof(poly_key));
return -1;
}
rc = nostr_poly1305_mac(poly_key, poly_in, poly_in_len, tag);
memset(poly_key, 0, sizeof(poly_key));
memset(block0, 0, sizeof(block0));
memset(poly_in, 0, poly_in_len);
free(poly_in);
return rc;
}
int nostr_chacha20poly1305_decrypt(const unsigned char key[32],
const unsigned char nonce[12],
const unsigned char *aad, size_t aad_len,
const unsigned char *ciphertext, size_t ct_len,
const unsigned char tag[16],
unsigned char *plaintext) {
unsigned char block0[64];
unsigned char poly_key[32];
unsigned char expected_tag[16];
unsigned char *poly_in = NULL;
size_t poly_in_len = 0;
int rc;
if (!key || !nonce || !tag || !plaintext || (!ciphertext && ct_len != 0) || (!aad && aad_len != 0)) {
return -1;
}
if (chacha20_block(key, 0, nonce, block0) != 0) {
return -1;
}
memcpy(poly_key, block0, sizeof(poly_key));
rc = build_poly1305_input(aad, aad_len, ciphertext, ct_len, &poly_in, &poly_in_len);
if (rc != 0) {
memset(poly_key, 0, sizeof(poly_key));
return -1;
}
rc = nostr_poly1305_mac(poly_key, poly_in, poly_in_len, expected_tag);
if (rc != 0 || !constant_time_tag_eq(tag, expected_tag)) {
memset(poly_key, 0, sizeof(poly_key));
memset(block0, 0, sizeof(block0));
memset(expected_tag, 0, sizeof(expected_tag));
memset(poly_in, 0, poly_in_len);
free(poly_in);
return -1;
}
if (ct_len > 0) {
rc = chacha20_encrypt(key, 1, nonce, ciphertext, plaintext, ct_len);
if (rc != 0) {
memset(poly_key, 0, sizeof(poly_key));
memset(block0, 0, sizeof(block0));
memset(expected_tag, 0, sizeof(expected_tag));
memset(poly_in, 0, poly_in_len);
free(poly_in);
return -1;
}
}
memset(poly_key, 0, sizeof(poly_key));
memset(block0, 0, sizeof(block0));
memset(expected_tag, 0, sizeof(expected_tag));
memset(poly_in, 0, poly_in_len);
free(poly_in);
return 0;
}
+262
View File
@@ -0,0 +1,262 @@
/*
* nostr_poly1305.c - Poly1305 message authentication code
*
* Public-domain style 32-bit implementation based on the poly1305-donna
* approach, adapted for nostr_core_lib naming and conventions.
*
* RFC 8439 Section 2.5
*/
#include <stddef.h>
#include <stdint.h>
#include <string.h>
typedef struct {
uint32_t r0, r1, r2, r3, r4;
uint32_t s1, s2, s3, s4;
uint32_t h0, h1, h2, h3, h4;
uint32_t pad0, pad1, pad2, pad3;
size_t leftover;
unsigned char buffer[16];
unsigned char final;
} nostr_poly1305_ctx_t;
static uint32_t u8to32_le(const unsigned char *p) {
return ((uint32_t)p[0]) |
((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) |
((uint32_t)p[3] << 24);
}
static void u32to8_le(unsigned char *p, uint32_t v) {
p[0] = (unsigned char)(v & 0xff);
p[1] = (unsigned char)((v >> 8) & 0xff);
p[2] = (unsigned char)((v >> 16) & 0xff);
p[3] = (unsigned char)((v >> 24) & 0xff);
}
static void poly1305_blocks(nostr_poly1305_ctx_t *st, const unsigned char *m, size_t bytes) {
const uint32_t r0 = st->r0;
const uint32_t r1 = st->r1;
const uint32_t r2 = st->r2;
const uint32_t r3 = st->r3;
const uint32_t r4 = st->r4;
const uint32_t s1 = st->s1;
const uint32_t s2 = st->s2;
const uint32_t s3 = st->s3;
const uint32_t s4 = st->s4;
uint32_t h0 = st->h0;
uint32_t h1 = st->h1;
uint32_t h2 = st->h2;
uint32_t h3 = st->h3;
uint32_t h4 = st->h4;
const uint32_t hibit = st->final ? 0 : (1U << 24);
while (bytes >= 16) {
uint32_t t0 = u8to32_le(m + 0);
uint32_t t1 = u8to32_le(m + 4);
uint32_t t2 = u8to32_le(m + 8);
uint32_t t3 = u8to32_le(m + 12);
uint64_t d0, d1, d2, d3, d4;
uint32_t c;
h0 += ( t0 ) & 0x3ffffff;
h1 += (((t1 << 6) | (t0 >> 26)) ) & 0x3ffffff;
h2 += (((t2 << 12) | (t1 >> 20))) & 0x3ffffff;
h3 += (((t3 << 18) | (t2 >> 14))) & 0x3ffffff;
h4 += (( t3 >> 8) ) & 0x00ffffff;
h4 += hibit;
d0 = ((uint64_t)h0 * r0) + ((uint64_t)h1 * s4) + ((uint64_t)h2 * s3) + ((uint64_t)h3 * s2) + ((uint64_t)h4 * s1);
d1 = ((uint64_t)h0 * r1) + ((uint64_t)h1 * r0) + ((uint64_t)h2 * s4) + ((uint64_t)h3 * s3) + ((uint64_t)h4 * s2);
d2 = ((uint64_t)h0 * r2) + ((uint64_t)h1 * r1) + ((uint64_t)h2 * r0) + ((uint64_t)h3 * s4) + ((uint64_t)h4 * s3);
d3 = ((uint64_t)h0 * r3) + ((uint64_t)h1 * r2) + ((uint64_t)h2 * r1) + ((uint64_t)h3 * r0) + ((uint64_t)h4 * s4);
d4 = ((uint64_t)h0 * r4) + ((uint64_t)h1 * r3) + ((uint64_t)h2 * r2) + ((uint64_t)h3 * r1) + ((uint64_t)h4 * r0);
c = (uint32_t)(d0 >> 26); h0 = (uint32_t)d0 & 0x3ffffff;
d1 += c;
c = (uint32_t)(d1 >> 26); h1 = (uint32_t)d1 & 0x3ffffff;
d2 += c;
c = (uint32_t)(d2 >> 26); h2 = (uint32_t)d2 & 0x3ffffff;
d3 += c;
c = (uint32_t)(d3 >> 26); h3 = (uint32_t)d3 & 0x3ffffff;
d4 += c;
c = (uint32_t)(d4 >> 26); h4 = (uint32_t)d4 & 0x3ffffff;
h0 += c * 5;
c = h0 >> 26; h0 &= 0x3ffffff;
h1 += c;
m += 16;
bytes -= 16;
}
st->h0 = h0;
st->h1 = h1;
st->h2 = h2;
st->h3 = h3;
st->h4 = h4;
}
void nostr_poly1305_init(nostr_poly1305_ctx_t *st, const unsigned char key[32]) {
uint32_t t0, t1, t2, t3;
t0 = u8to32_le(key + 0);
t1 = u8to32_le(key + 4);
t2 = u8to32_le(key + 8);
t3 = u8to32_le(key + 12);
st->r0 = ( t0 ) & 0x3ffffff;
st->r1 = (((t1 << 6) | (t0 >> 26)) ) & 0x3ffff03;
st->r2 = (((t2 << 12) | (t1 >> 20))) & 0x3ffc0ff;
st->r3 = (((t3 << 18) | (t2 >> 14))) & 0x3f03fff;
st->r4 = (( t3 >> 8) ) & 0x00fffff;
st->s1 = st->r1 * 5;
st->s2 = st->r2 * 5;
st->s3 = st->r3 * 5;
st->s4 = st->r4 * 5;
st->h0 = 0;
st->h1 = 0;
st->h2 = 0;
st->h3 = 0;
st->h4 = 0;
st->pad0 = u8to32_le(key + 16);
st->pad1 = u8to32_le(key + 20);
st->pad2 = u8to32_le(key + 24);
st->pad3 = u8to32_le(key + 28);
st->leftover = 0;
st->final = 0;
}
void nostr_poly1305_update(nostr_poly1305_ctx_t *st, const unsigned char *m, size_t bytes) {
size_t i;
if (st->leftover) {
size_t want = (size_t)(16 - st->leftover);
if (want > bytes) {
want = bytes;
}
for (i = 0; i < want; i++) {
st->buffer[st->leftover + i] = m[i];
}
bytes -= want;
m += want;
st->leftover += want;
if (st->leftover < 16) {
return;
}
poly1305_blocks(st, st->buffer, 16);
st->leftover = 0;
}
if (bytes >= 16) {
size_t want = bytes & ~(size_t)0xf;
poly1305_blocks(st, m, want);
m += want;
bytes -= want;
}
if (bytes) {
for (i = 0; i < bytes; i++) {
st->buffer[st->leftover + i] = m[i];
}
st->leftover += bytes;
}
}
void nostr_poly1305_final(nostr_poly1305_ctx_t *st, unsigned char tag[16]) {
uint32_t h0, h1, h2, h3, h4, c;
uint32_t g0, g1, g2, g3, g4;
uint64_t f;
uint32_t mask;
if (st->leftover) {
size_t i = st->leftover;
st->buffer[i++] = 1;
for (; i < 16; i++) {
st->buffer[i] = 0;
}
st->final = 1;
poly1305_blocks(st, st->buffer, 16);
}
h0 = st->h0;
h1 = st->h1;
h2 = st->h2;
h3 = st->h3;
h4 = st->h4;
c = h1 >> 26; h1 &= 0x3ffffff;
h2 += c;
c = h2 >> 26; h2 &= 0x3ffffff;
h3 += c;
c = h3 >> 26; h3 &= 0x3ffffff;
h4 += c;
c = h4 >> 26; h4 &= 0x3ffffff;
h0 += c * 5;
c = h0 >> 26; h0 &= 0x3ffffff;
h1 += c;
g0 = h0 + 5;
c = g0 >> 26; g0 &= 0x3ffffff;
g1 = h1 + c;
c = g1 >> 26; g1 &= 0x3ffffff;
g2 = h2 + c;
c = g2 >> 26; g2 &= 0x3ffffff;
g3 = h3 + c;
c = g3 >> 26; g3 &= 0x3ffffff;
g4 = h4 + c - (1U << 26);
mask = (g4 >> 31) - 1U;
g0 &= mask;
g1 &= mask;
g2 &= mask;
g3 &= mask;
g4 &= mask;
mask = ~mask;
h0 = (h0 & mask) | g0;
h1 = (h1 & mask) | g1;
h2 = (h2 & mask) | g2;
h3 = (h3 & mask) | g3;
h4 = (h4 & mask) | g4;
h0 = ((h0 ) | (h1 << 26)) & 0xffffffff;
h1 = ((h1 >> 6 ) | (h2 << 20)) & 0xffffffff;
h2 = ((h2 >> 12) | (h3 << 14)) & 0xffffffff;
h3 = ((h3 >> 18) | (h4 << 8 )) & 0xffffffff;
f = (uint64_t)h0 + st->pad0;
h0 = (uint32_t)f;
f = (uint64_t)h1 + st->pad1 + (f >> 32);
h1 = (uint32_t)f;
f = (uint64_t)h2 + st->pad2 + (f >> 32);
h2 = (uint32_t)f;
f = (uint64_t)h3 + st->pad3 + (f >> 32);
h3 = (uint32_t)f;
u32to8_le(tag + 0, h0);
u32to8_le(tag + 4, h1);
u32to8_le(tag + 8, h2);
u32to8_le(tag + 12, h3);
memset(st, 0, sizeof(*st));
}
int nostr_poly1305_mac(const unsigned char key[32], const unsigned char *msg,
size_t msg_len, unsigned char tag[16]) {
nostr_poly1305_ctx_t ctx;
if (!key || (!msg && msg_len != 0) || !tag) {
return -1;
}
nostr_poly1305_init(&ctx, key);
nostr_poly1305_update(&ctx, msg, msg_len);
nostr_poly1305_final(&ctx, tag);
return 0;
}
+75 -19
View File
@@ -3,9 +3,8 @@
#include <secp256k1_ecdh.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <stddef.h>
#include "../nostr_platform.h"
/*
* PRIVATE INTERNAL FUNCTIONS - NOT EXPORTED
@@ -237,28 +236,85 @@ int nostr_secp256k1_ecdh(unsigned char *result, const nostr_secp256k1_pubkey *pu
return secp256k1_ecdh(g_ctx, result, &internal_pubkey, seckey, hashfp, data);
}
int nostr_secp256k1_ec_pubkey_tweak_mul(nostr_secp256k1_pubkey* pubkey, const unsigned char* tweak32) {
if (g_ctx == NULL || pubkey == NULL || tweak32 == NULL) {
return 0;
}
secp256k1_pubkey internal_pubkey;
memcpy(&internal_pubkey, pubkey->data, sizeof(secp256k1_pubkey));
if (!secp256k1_ec_pubkey_tweak_mul(g_ctx, &internal_pubkey, tweak32)) {
return 0;
}
memcpy(pubkey->data, &internal_pubkey, sizeof(secp256k1_pubkey));
return 1;
}
int nostr_secp256k1_ec_pubkey_negate(nostr_secp256k1_pubkey* pubkey) {
if (g_ctx == NULL || pubkey == NULL) {
return 0;
}
secp256k1_pubkey internal_pubkey;
memcpy(&internal_pubkey, pubkey->data, sizeof(secp256k1_pubkey));
if (!secp256k1_ec_pubkey_negate(g_ctx, &internal_pubkey)) {
return 0;
}
memcpy(pubkey->data, &internal_pubkey, sizeof(secp256k1_pubkey));
return 1;
}
int nostr_secp256k1_ec_pubkey_combine(nostr_secp256k1_pubkey* out,
const nostr_secp256k1_pubkey* const* ins,
size_t n) {
if (g_ctx == NULL || out == NULL || ins == NULL || n == 0) {
return 0;
}
secp256k1_pubkey* parsed = (secp256k1_pubkey*)calloc(n, sizeof(secp256k1_pubkey));
const secp256k1_pubkey** ptrs = (const secp256k1_pubkey**)calloc(n, sizeof(secp256k1_pubkey*));
if (!parsed || !ptrs) {
free(parsed);
free(ptrs);
return 0;
}
for (size_t i = 0; i < n; i++) {
if (!ins[i]) {
free(parsed);
free(ptrs);
return 0;
}
memcpy(&parsed[i], ins[i]->data, sizeof(secp256k1_pubkey));
ptrs[i] = &parsed[i];
}
secp256k1_pubkey combined;
int ok = secp256k1_ec_pubkey_combine(g_ctx, &combined, ptrs, n);
free(parsed);
free(ptrs);
if (!ok) {
return 0;
}
memcpy(out->data, &combined, sizeof(secp256k1_pubkey));
return 1;
}
int nostr_secp256k1_get_random_bytes(unsigned char *buf, size_t len) {
if (buf == NULL || len == 0) {
return 0;
}
// Try to use /dev/urandom for good randomness
int fd = open("/dev/urandom", O_RDONLY);
if (fd >= 0) {
ssize_t result = read(fd, buf, len);
close(fd);
if (result == (ssize_t)len) {
return 1;
}
if (nostr_platform_random(buf, len) != 0) {
return 0;
}
// Fallback to a simple PRNG (not cryptographically secure, but better than nothing)
// In a real implementation, you'd want to use a proper CSPRNG
static unsigned long seed = 1;
for (size_t i = 0; i < len; i++) {
seed = seed * 1103515245 + 12345;
buf[i] = (unsigned char)(seed >> 16);
}
return 1;
}
+197
View File
@@ -0,0 +1,197 @@
/*
* NOSTR Core Library - NIP-03: OpenTimestamps Attestations for Events
*/
#include "nip003.h"
#include "nip001.h"
#include "nostr_http.h"
#include "utils.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
cJSON* nostr_nip03_create_proof_event(const char* target_event_id,
int target_event_kind,
const char* ots_data_base64,
const char* relay_url,
const unsigned char* private_key) {
if (!target_event_id || !ots_data_base64 || !private_key) return NULL;
cJSON* tags = cJSON_CreateArray();
// ["e", <target-event-id>, <relay-url>]
cJSON* e_tag = cJSON_CreateArray();
cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
cJSON_AddItemToArray(e_tag, cJSON_CreateString(target_event_id));
if (relay_url) {
cJSON_AddItemToArray(e_tag, cJSON_CreateString(relay_url));
}
cJSON_AddItemToArray(tags, e_tag);
// ["k", "<target-event-kind>"]
char kind_str[16];
snprintf(kind_str, sizeof(kind_str), "%d", target_event_kind);
cJSON* k_tag = cJSON_CreateArray();
cJSON_AddItemToArray(k_tag, cJSON_CreateString("k"));
cJSON_AddItemToArray(k_tag, cJSON_CreateString(kind_str));
cJSON_AddItemToArray(tags, k_tag);
cJSON* event = nostr_create_and_sign_event(NOSTR_KIND_OT_PROOF, ots_data_base64, tags, private_key, 0);
cJSON_Delete(tags);
return event;
}
char* nostr_nip03_request_timestamp(const char* event_id_hex,
const char* calendar_url,
int timeout_seconds) {
if (!event_id_hex || !calendar_url) return NULL;
// Convert hex event ID to binary digest
unsigned char digest[32];
if (nostr_hex_to_bytes(event_id_hex, digest, 32) != 0) {
return NULL;
}
// OTS calendar digest submission endpoint: POST /digest
// The body is the raw 32-byte digest.
char full_url[512];
snprintf(full_url, sizeof(full_url), "%s/digest", calendar_url);
nostr_http_request_t req = {0};
req.method = "POST";
req.url = full_url;
req.body = digest;
req.body_len = 32;
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 10;
req.follow_redirects = 1;
req.max_redirects = 3;
const char* headers[] = {
"Content-Type: application/octet-stream",
"Accept: application/octet-stream",
NULL
};
req.headers = headers;
nostr_http_response_t resp = {0};
if (nostr_http_request(&req, &resp) != NOSTR_SUCCESS) {
return NULL;
}
if (resp.status_code != 200 || !resp.body || resp.body_len == 0) {
nostr_http_response_free(&resp);
return NULL;
}
// The response is the raw .ots file data.
// We need to base64 encode it for the Nostr event content.
size_t b64_size = ((resp.body_len + 2) / 3) * 4 + 1;
char* b64 = (char*)malloc(b64_size);
if (b64) {
base64_encode((const unsigned char*)resp.body, resp.body_len, b64, b64_size);
}
nostr_http_response_free(&resp);
return b64;
}
int nostr_nip03_is_proof_complete(const char* ots_data_base64) {
if (!ots_data_base64) return -1;
size_t input_len = strlen(ots_data_base64);
size_t max_decoded_len = (input_len / 4) * 3 + 3;
unsigned char* data = (unsigned char*)malloc(max_decoded_len);
if (!data) return -1;
size_t len = base64_decode(ots_data_base64, data);
if (len == 0) {
free(data);
return -1;
}
// Very simple heuristic for OTS file completeness:
// A complete OTS file contains a Bitcoin attestation.
// The Bitcoin attestation tag in OTS is 0x05 (Pending) vs 0x00 (Bitcoin).
// This is a simplification; a real OTS parser would be better.
// For now, we look for the Bitcoin block header attestation tag.
int complete = 0;
// OTS files start with a specific header: "\x00OpenTimestamps\x00\x00\x03\x01"
// We just check if it contains the Bitcoin attestation tag (0x00)
// followed by the Bitcoin genesis block hash or similar markers.
// Actually, the easiest way is to check if it's NOT just a pending attestation.
// For the purpose of this library, we'll assume if it's > 100 bytes it likely has an attestation.
// Initial pending proofs are usually very small (~40-60 bytes).
if (len > 150) {
complete = 1;
}
free(data);
return complete;
}
char* nostr_nip03_upgrade_proof(const char* ots_data_base64,
const char* calendar_url,
int timeout_seconds) {
if (!ots_data_base64 || !calendar_url) return NULL;
size_t input_len = strlen(ots_data_base64);
size_t max_decoded_len = (input_len / 4) * 3 + 3;
unsigned char* data = (unsigned char*)malloc(max_decoded_len);
if (!data) return NULL;
size_t len = base64_decode(ots_data_base64, data);
if (len == 0) {
free(data);
return NULL;
}
// OTS upgrade endpoint: POST /upgrade
// Body is the current .ots file content.
char full_url[512];
snprintf(full_url, sizeof(full_url), "%s/upgrade", calendar_url);
nostr_http_request_t req = {0};
req.method = "POST";
req.url = full_url;
req.body = data;
req.body_len = len;
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 10;
req.follow_redirects = 1;
req.max_redirects = 3;
const char* headers[] = {
"Content-Type: application/octet-stream",
"Accept: application/octet-stream",
NULL
};
req.headers = headers;
nostr_http_response_t resp = {0};
int ret = nostr_http_request(&req, &resp);
free(data);
if (ret != NOSTR_SUCCESS || resp.status_code != 200 || !resp.body || resp.body_len == 0) {
nostr_http_response_free(&resp);
return NULL;
}
// If the response is the same length as input, it might not be upgraded yet
if (resp.body_len <= len) {
nostr_http_response_free(&resp);
return NULL;
}
size_t b64_size = ((resp.body_len + 2) / 3) * 4 + 1;
char* b64 = (char*)malloc(b64_size);
if (b64) {
base64_encode((const unsigned char*)resp.body, resp.body_len, b64, b64_size);
}
nostr_http_response_free(&resp);
return b64;
}
+72
View File
@@ -0,0 +1,72 @@
/*
* NOSTR Core Library - NIP-03: OpenTimestamps Attestations for Events
*/
#ifndef NOSTR_NIP03_H
#define NOSTR_NIP03_H
#include "nostr_common.h"
#include "cjson/cJSON.h"
#ifdef __cplusplus
extern "C" {
#endif
#define NOSTR_KIND_OT_PROOF 1040
/**
* Create a NIP-03 OpenTimestamps proof event (kind 1040)
*
* @param target_event_id The ID of the event being timestamped (hex string)
* @param target_event_kind The kind of the event being timestamped
* @param ots_data_base64 Base64 encoded .ots file content
* @param relay_url Optional relay URL where the target event can be found
* @param private_key 32-byte private key for signing the proof event
* @return cJSON* The signed proof event, or NULL on error
*/
cJSON* nostr_nip03_create_proof_event(const char* target_event_id,
int target_event_kind,
const char* ots_data_base64,
const char* relay_url,
const unsigned char* private_key);
/**
* Request a timestamp for an event ID from an OpenTimestamps calendar
*
* This function sends the event ID (as a SHA256 digest) to an OTS calendar
* and returns the initial .ots file data (base64 encoded).
*
* @param event_id_hex The event ID to timestamp (64-char hex string)
* @param calendar_url The OTS calendar URL (e.g., "https://alice.btc.calendar.opentimestamps.org")
* @param timeout_seconds Request timeout
* @return char* Base64 encoded .ots data, or NULL on error (caller must free)
*/
char* nostr_nip03_request_timestamp(const char* event_id_hex,
const char* calendar_url,
int timeout_seconds);
/**
* Check if an OTS proof is complete (has a Bitcoin attestation)
*
* @param ots_data_base64 Base64 encoded .ots file content
* @return int 1 if complete, 0 if pending, -1 on error
*/
int nostr_nip03_is_proof_complete(const char* ots_data_base64);
/**
* Upgrade an OTS proof by fetching missing attestations from a calendar
*
* @param ots_data_base64 Current base64 encoded .ots file content
* @param calendar_url The OTS calendar URL
* @param timeout_seconds Request timeout
* @return char* New base64 encoded .ots data, or NULL on error/no change (caller must free)
*/
char* nostr_nip03_upgrade_proof(const char* ots_data_base64,
const char* calendar_url,
int timeout_seconds);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_NIP03_H */
+13 -79
View File
@@ -12,46 +12,7 @@
#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;
}
#include "nostr_http.h"
/**
* Parse and validate a NIP-05 identifier into local part and domain
@@ -104,49 +65,22 @@ static int nip05_http_get(const char* url, int timeout_seconds, char** response_
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);
long status = 0;
int rc = nostr_http_get(url,
timeout_seconds > 0 ? timeout_seconds : NIP05_DEFAULT_TIMEOUT,
response_data,
&status);
if (rc != NOSTR_SUCCESS) {
return NOSTR_ERROR_NIP05_HTTP_FAILED;
}
if (response_code != 200) {
free(response.data);
if (status != 200) {
free(*response_data);
*response_data = NULL;
return NOSTR_ERROR_NIP05_HTTP_FAILED;
}
*response_data = response.data;
return NOSTR_SUCCESS;
}
+3 -17
View File
@@ -10,24 +10,17 @@
#include <ctype.h>
#include <time.h>
#include "../nostr_core/nostr_common.h"
#include "nostr_platform.h"
int nostr_generate_keypair(unsigned char* private_key, unsigned char* public_key) {
if (!private_key || !public_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Generate random entropy using /dev/urandom
FILE* urandom = fopen("/dev/urandom", "rb");
if (!urandom) {
if (nostr_platform_random(private_key, 32) != 0) {
return NOSTR_ERROR_IO_FAILED;
}
if (fread(private_key, 1, 32, urandom) != 32) {
fclose(urandom);
return NOSTR_ERROR_IO_FAILED;
}
fclose(urandom);
// Validate private key
if (nostr_ec_private_key_verify(private_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
@@ -50,17 +43,10 @@ int nostr_generate_mnemonic_and_keys(char* mnemonic, size_t mnemonic_size,
// Generate entropy for 12-word mnemonic
unsigned char entropy[16];
FILE* urandom = fopen("/dev/urandom", "rb");
if (!urandom) {
if (nostr_platform_random(entropy, sizeof(entropy)) != 0) {
return NOSTR_ERROR_IO_FAILED;
}
if (fread(entropy, 1, sizeof(entropy), urandom) != sizeof(entropy)) {
fclose(urandom);
return NOSTR_ERROR_IO_FAILED;
}
fclose(urandom);
// Generate mnemonic from entropy
if (nostr_bip39_mnemonic_from_bytes(entropy, sizeof(entropy), mnemonic) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
+25 -87
View File
@@ -11,50 +11,13 @@
#ifndef DISABLE_NIP05 // NIP-11 uses the same HTTP infrastructure as NIP-05
#include <curl/curl.h>
#include "nostr_http.h"
// Maximum sizes for NIP-11 operations
#define NIP11_MAX_URL_SIZE 512
#define NIP11_MAX_RESPONSE_SIZE 16384
#define NIP11_DEFAULT_TIMEOUT 10
// Structure for HTTP response handling (same as NIP-05)
typedef struct {
char* data;
size_t size;
size_t capacity;
} nip11_http_response_t;
/**
* Callback function for curl to write HTTP response data
*/
static size_t nip11_write_callback(void* contents, size_t size, size_t nmemb, nip11_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;
}
/**
* Convert WebSocket URL to HTTP URL for NIP-11 document retrieval
*/
@@ -341,60 +304,35 @@ int nostr_nip11_fetch_relay_info(const char* relay_url, nostr_relay_info_t** inf
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;
}
// Use the HTTP response structure
nip11_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 - use proper function pointer cast
curl_easy_setopt(curl, CURLOPT_URL, http_url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)nip11_write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)(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);
const char* headers[] = {
"Accept: application/nostr+json",
NULL
};
nostr_http_request_t req;
memset(&req, 0, sizeof(req));
req.method = "GET";
req.url = http_url;
req.headers = headers;
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : NIP11_DEFAULT_TIMEOUT;
req.follow_redirects = 1;
req.max_redirects = 3;
nostr_http_response_t response;
int http_rc = nostr_http_request(&req, &response);
free(http_url);
if (res != CURLE_OK || response_code != 200) {
free(response.data);
if (http_rc != NOSTR_SUCCESS || response.status_code != 200) {
if (http_rc == NOSTR_SUCCESS) {
nostr_http_response_free(&response);
}
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);
int parse_result = nip11_parse_relay_info(response.body, info);
nostr_http_response_free(&response);
if (parse_result != NOSTR_SUCCESS) {
nostr_nip11_relay_info_free(info);
+16
View File
@@ -354,6 +354,22 @@ cJSON* nostr_nip17_receive_dm(cJSON* gift_wrap,
// Unseal the rumor
cJSON* rumor = nostr_nip59_unseal_rumor(seal, sender_public_key, recipient_private_key);
cJSON_Delete(seal); // Seal is no longer needed
if (!rumor) {
return NULL;
}
// NIP-17 safety check: seal pubkey must match rumor pubkey to prevent impersonation
cJSON* rumor_pubkey_item = cJSON_GetObjectItem(rumor, "pubkey");
if (!rumor_pubkey_item || !cJSON_IsString(rumor_pubkey_item)) {
cJSON_Delete(rumor);
return NULL;
}
const char* rumor_pubkey_hex = cJSON_GetStringValue(rumor_pubkey_item);
if (!rumor_pubkey_hex || strcmp(sender_pubkey_hex, rumor_pubkey_hex) != 0) {
cJSON_Delete(rumor);
return NULL;
}
return rumor;
}
+1 -1
View File
@@ -180,7 +180,7 @@ nostr_input_type_t nostr_detect_input_type(const char* input) {
if (len == 64) {
int is_hex = 1;
for (size_t i = 0; i < len; i++) {
if (!isxdigit(input[i])) {
if (!isxdigit((unsigned char)input[i])) {
is_hex = 0;
break;
}
+86 -8
View File
@@ -41,6 +41,39 @@ static time_t random_past_timestamp(long max_delay_sec) {
return now - random_offset;
}
/**
* NIP-44 padding calculation (mirrors nip044.c for output sizing)
*/
static size_t calc_nip44_padded_len(size_t unpadded_len) {
if (unpadded_len <= 32) {
return 32;
}
size_t next_power = 1;
while (next_power < unpadded_len) {
next_power <<= 1;
}
size_t chunk = (next_power <= 256) ? 32 : (next_power / 8);
return chunk * ((unpadded_len - 1) / chunk + 1);
}
/**
* Calculate safe output buffer size for NIP-44 encrypted base64 payload.
* Returns 0 if plaintext length exceeds NIP-44 max.
*/
static size_t calc_nip44_encrypted_b64_size(size_t plaintext_len) {
if (plaintext_len > NOSTR_NIP44_MAX_PLAINTEXT_SIZE) {
return 0;
}
size_t padded_len = calc_nip44_padded_len(plaintext_len) + 2; // +2 for length prefix
size_t payload_len = 1 + 32 + padded_len + 32; // version + nonce + ciphertext + mac
size_t b64_len = ((payload_len + 2) / 3) * 4 + 1; // +1 for NUL terminator
return b64_len;
}
/**
* Generate a random private key for gift wrap
*/
@@ -160,18 +193,31 @@ cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private
}
// Encrypt the rumor using NIP-44
char encrypted_content[4096]; // Should be large enough for most events
size_t encrypted_size = calc_nip44_encrypted_b64_size(strlen(rumor_json));
if (encrypted_size == 0) {
free(rumor_json);
return NULL;
}
char* encrypted_content = malloc(encrypted_size);
if (!encrypted_content) {
free(rumor_json);
return NULL;
}
int encrypt_result = nostr_nip44_encrypt(sender_private_key, recipient_public_key,
rumor_json, encrypted_content, sizeof(encrypted_content));
rumor_json, encrypted_content, encrypted_size);
free(rumor_json);
if (encrypt_result != NOSTR_SUCCESS) {
free(encrypted_content);
return NULL;
}
// Get sender's public key
unsigned char sender_public_key[32];
if (nostr_ec_public_key_from_private_key(sender_private_key, sender_public_key) != 0) {
free(encrypted_content);
return NULL;
}
@@ -181,6 +227,7 @@ cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private
// Create seal event (kind 13)
cJSON* seal = cJSON_CreateObject();
if (!seal) {
free(encrypted_content);
return NULL;
}
@@ -191,6 +238,7 @@ cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private
cJSON_AddNumberToObject(seal, "kind", 13);
cJSON_AddItemToObject(seal, "tags", cJSON_CreateArray()); // Empty tags array
cJSON_AddStringToObject(seal, "content", encrypted_content);
free(encrypted_content);
// Calculate event ID
char event_id[65];
@@ -261,13 +309,27 @@ cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_ke
}
// Encrypt the seal using NIP-44
char encrypted_content[8192]; // Larger buffer for nested encryption
size_t encrypted_size = calc_nip44_encrypted_b64_size(strlen(seal_json));
if (encrypted_size == 0) {
memory_clear(random_private_key, 32);
free(seal_json);
return NULL;
}
char* encrypted_content = malloc(encrypted_size);
if (!encrypted_content) {
memory_clear(random_private_key, 32);
free(seal_json);
return NULL;
}
int encrypt_result = nostr_nip44_encrypt(random_private_key, recipient_public_key,
seal_json, encrypted_content, sizeof(encrypted_content));
seal_json, encrypted_content, encrypted_size);
free(seal_json);
if (encrypt_result != NOSTR_SUCCESS) {
memory_clear(random_private_key, 32);
free(encrypted_content);
return NULL;
}
@@ -275,6 +337,7 @@ cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_ke
cJSON* gift_wrap = cJSON_CreateObject();
if (!gift_wrap) {
memory_clear(random_private_key, 32);
free(encrypted_content);
return NULL;
}
@@ -293,6 +356,7 @@ cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_ke
cJSON_AddItemToObject(gift_wrap, "tags", tags);
cJSON_AddStringToObject(gift_wrap, "content", encrypted_content);
free(encrypted_content);
// Calculate event ID
char event_id[65];
@@ -359,16 +423,23 @@ cJSON* nostr_nip59_unwrap_gift(cJSON* gift_wrap, const unsigned char* recipient_
}
// Decrypt the content using NIP-44
char decrypted_json[8192];
size_t decrypted_size = strlen(encrypted_content) + 1;
char* decrypted_json = malloc(decrypted_size);
if (!decrypted_json) {
return NULL;
}
int decrypt_result = nostr_nip44_decrypt(recipient_private_key, sender_public_key,
encrypted_content, decrypted_json, sizeof(decrypted_json));
encrypted_content, decrypted_json, decrypted_size);
if (decrypt_result != NOSTR_SUCCESS) {
free(decrypted_json);
return NULL;
}
// Parse the decrypted JSON as the seal event
cJSON* seal = cJSON_Parse(decrypted_json);
free(decrypted_json);
if (!seal) {
return NULL;
}
@@ -394,16 +465,23 @@ cJSON* nostr_nip59_unseal_rumor(cJSON* seal, const unsigned char* sender_public_
const char* encrypted_content = cJSON_GetStringValue(content_item);
// Decrypt the content using NIP-44
char decrypted_json[4096];
size_t decrypted_size = strlen(encrypted_content) + 1;
char* decrypted_json = malloc(decrypted_size);
if (!decrypted_json) {
return NULL;
}
int decrypt_result = nostr_nip44_decrypt(recipient_private_key, sender_public_key,
encrypted_content, decrypted_json, sizeof(decrypted_json));
encrypted_content, decrypted_json, decrypted_size);
if (decrypt_result != NOSTR_SUCCESS) {
free(decrypted_json);
return NULL;
}
// Parse the decrypted JSON as the rumor event
cJSON* rumor = cJSON_Parse(decrypted_json);
free(decrypted_json);
if (!rumor) {
return NULL;
}
+1 -1
View File
@@ -22,7 +22,7 @@ extern "C" {
#define NOSTR_NIP60_HISTORY_KIND 7376
#define NOSTR_NIP60_QUOTE_KIND 7374
#define NOSTR_CASHU_KEYSET_ID_HEX_SIZE 17
#define NOSTR_CASHU_KEYSET_ID_HEX_SIZE 65
#define NOSTR_CASHU_PUBKEY_HEX_SIZE 67
#define NOSTR_CASHU_EVENT_ID_HEX_SIZE 65
+15 -10
View File
@@ -2,10 +2,10 @@
#define NOSTR_CORE_H
// Version information (auto-updated by increment_and_push.sh)
#define VERSION "v0.5.4"
#define VERSION "v0.6.2"
#define VERSION_MAJOR 0
#define VERSION_MINOR 5
#define VERSION_PATCH 4
#define VERSION_MINOR 6
#define VERSION_PATCH 2
/*
* NOSTR Core Library - Complete API Reference
@@ -177,6 +177,7 @@ extern "C" {
// NIP implementations
#include "nip001.h" // Basic Protocol
#include "nip003.h" // OpenTimestamps
#include "nip004.h" // Encryption (legacy)
#include "nip005.h" // DNS-based identifiers
#include "nip006.h" // Key derivation from mnemonic
@@ -193,6 +194,9 @@ extern "C" {
#include "nip061.h" // Nutzaps
#include "cashu_mint.h" // Cashu mint HTTP client
#include "nostr_http.h" // Shared HTTP client
#include "blossom_client.h" // Blossom HTTP client
// Authentication and request validation system
#include "request_validator.h" // Request validation and authentication rules
@@ -241,13 +245,14 @@ typedef struct nostr_pool_subscription nostr_pool_subscription_t;
// Reconnection configuration
typedef struct {
int enable_auto_reconnect; // 1 = enable, 0 = disable
int max_reconnect_attempts; // Max attempts per relay
int initial_reconnect_delay_ms; // Initial delay between attempts
int max_reconnect_delay_ms; // Max delay (cap exponential backoff)
int reconnect_backoff_multiplier; // Delay multiplier
int ping_interval_seconds; // How often to ping (0 = disable)
int pong_timeout_seconds; // How long to wait for pong before reconnecting
int enable_auto_reconnect; // 1 = enable, 0 = disable
int max_reconnect_attempts; // Max attempts per relay
int initial_reconnect_delay_ms; // Initial delay between attempts
int max_reconnect_delay_ms; // Max delay (cap exponential backoff)
int reconnect_backoff_multiplier; // Delay multiplier
int reconnect_reset_stability_seconds; // Connected time required before resetting reconnect_attempts
int ping_interval_seconds; // How often to ping (0 = disable)
int pong_timeout_seconds; // How long to wait for pong before reconnecting
} nostr_pool_reconnect_config_t;
// Relay pool management functions
+295
View File
@@ -0,0 +1,295 @@
/*
* NOSTR Core Library - Shared HTTP Client
*/
#include "nostr_http.h"
#include <curl/curl.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
typedef struct {
char* data;
size_t len;
size_t cap;
size_t max_bytes;
int truncated;
} nostr_http_buffer_t;
static char* nostr_http_strdup_local(const char* s) {
if (!s) return NULL;
size_t n = strlen(s);
char* out = (char*)malloc(n + 1U);
if (!out) return NULL;
memcpy(out, s, n + 1U);
return out;
}
static char g_ca_bundle_path[512] = {0};
static int append_bytes(nostr_http_buffer_t* b, const void* src, size_t n) {
if (!b || !src || n == 0) return 1;
if (b->max_bytes > 0 && b->len >= b->max_bytes) {
b->truncated = 1;
return 1;
}
size_t to_copy = n;
if (b->max_bytes > 0) {
size_t remaining = b->max_bytes - b->len;
if (to_copy > remaining) {
to_copy = remaining;
b->truncated = 1;
}
}
if (b->len + to_copy + 1U > b->cap) {
size_t new_cap = b->cap == 0 ? 1024U : b->cap;
while (new_cap < b->len + to_copy + 1U) {
new_cap *= 2U;
}
char* p = (char*)realloc(b->data, new_cap);
if (!p) return 0;
b->data = p;
b->cap = new_cap;
}
memcpy(b->data + b->len, src, to_copy);
b->len += to_copy;
b->data[b->len] = '\0';
return 1;
}
static size_t write_cb(void* contents, size_t size, size_t nmemb, void* userp) {
nostr_http_buffer_t* rb = (nostr_http_buffer_t*)userp;
size_t total = size * nmemb;
if (!rb || total == 0) return total;
if (!append_bytes(rb, contents, total)) return 0;
return total;
}
static size_t header_cb(void* contents, size_t size, size_t nmemb, void* userp) {
nostr_http_buffer_t* hb = (nostr_http_buffer_t*)userp;
size_t total = size * nmemb;
if (!hb || total == 0) return total;
if (!append_bytes(hb, contents, total)) return 0;
return total;
}
void nostr_http_set_ca_bundle(const char* ca_bundle_path) {
if (!ca_bundle_path || ca_bundle_path[0] == '\0') {
g_ca_bundle_path[0] = '\0';
return;
}
strncpy(g_ca_bundle_path, ca_bundle_path, sizeof(g_ca_bundle_path) - 1);
g_ca_bundle_path[sizeof(g_ca_bundle_path) - 1] = '\0';
}
const char* nostr_http_detect_ca_bundle(void) {
const char* env = getenv("SSL_CERT_FILE");
if (env && env[0] != '\0' && access(env, R_OK) == 0) {
return env;
}
static const char* candidates[] = {
"/etc/ssl/certs/ca-certificates.crt",
"/etc/ssl/cert.pem",
"/etc/pki/tls/certs/ca-bundle.crt",
"/etc/ssl/ca-bundle.pem"
};
for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
if (access(candidates[i], R_OK) == 0) {
return candidates[i];
}
}
return NULL;
}
int nostr_http_request(const nostr_http_request_t* req, nostr_http_response_t* resp) {
if (!req || !req->url || !resp) return NOSTR_ERROR_INVALID_INPUT;
memset(resp, 0, sizeof(*resp));
CURL* curl = curl_easy_init();
if (!curl) return NOSTR_ERROR_NETWORK_FAILED;
const char* method = (req->method && req->method[0] != '\0') ? req->method : "GET";
int timeout = req->timeout_seconds > 0 ? req->timeout_seconds : 30;
int follow = req->follow_redirects;
if (follow != 0 && follow != 1) follow = 1;
int max_redirs = req->max_redirects > 0 ? req->max_redirects : 3;
const char* user_agent = (req->user_agent && req->user_agent[0] != '\0') ? req->user_agent : "nostr-core/1.0";
nostr_http_buffer_t body = {0};
body.max_bytes = req->max_response_bytes;
nostr_http_buffer_t headers = {0};
struct curl_slist* header_list = NULL;
if (req->headers) {
for (const char** h = req->headers; *h; h++) {
if ((*h)[0] != '\0') header_list = curl_slist_append(header_list, *h);
}
}
curl_easy_setopt(curl, CURLOPT_URL, req->url);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, (long)follow);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, (long)max_redirs);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body);
if (req->capture_headers) {
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_cb);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headers);
}
if (header_list) {
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);
}
if (g_ca_bundle_path[0] != '\0') {
curl_easy_setopt(curl, CURLOPT_CAINFO, g_ca_bundle_path);
}
if (strcasecmp(method, "GET") == 0) {
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
} else if (strcasecmp(method, "POST") == 0) {
curl_easy_setopt(curl, CURLOPT_POST, 1L);
if (req->body && req->body_len > 0) {
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, (const char*)req->body);
} else {
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "");
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0L);
}
// Disable Expect: 100-continue
header_list = curl_slist_append(header_list, "Expect:");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);
} else if (strcasecmp(method, "HEAD") == 0) {
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "HEAD");
} else {
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method);
if (req->body && req->body_len > 0) {
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, (const char*)req->body);
}
}
CURLcode rc = curl_easy_perform(curl);
long status = 0;
char* content_type = NULL;
char* content_type_copy = NULL;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &content_type);
if (content_type && content_type[0] != '\0') {
content_type_copy = nostr_http_strdup_local(content_type);
}
if (header_list) curl_slist_free_all(header_list);
curl_easy_cleanup(curl);
if (rc != CURLE_OK) {
free(body.data);
free(headers.data);
free(content_type_copy);
return NOSTR_ERROR_NETWORK_FAILED;
}
resp->status_code = status;
resp->body = body.data ? body.data : nostr_http_strdup_local("");
resp->body_len = body.data ? body.len : 0;
resp->headers_raw = headers.data;
resp->content_type = content_type_copy;
resp->truncated = body.truncated;
if (!resp->body) {
free(resp->headers_raw);
free(resp->content_type);
memset(resp, 0, sizeof(*resp));
return NOSTR_ERROR_MEMORY_FAILED;
}
return NOSTR_SUCCESS;
}
void nostr_http_response_free(nostr_http_response_t* resp) {
if (!resp) return;
free(resp->body);
free(resp->content_type);
free(resp->headers_raw);
memset(resp, 0, sizeof(*resp));
}
int nostr_http_get(const char* url, int timeout_seconds, char** body_out, long* status_out) {
if (!url || !body_out) return NOSTR_ERROR_INVALID_INPUT;
*body_out = NULL;
if (status_out) *status_out = 0;
nostr_http_request_t req;
memset(&req, 0, sizeof(req));
req.method = "GET";
req.url = url;
req.timeout_seconds = timeout_seconds;
req.follow_redirects = 1;
req.max_redirects = 3;
nostr_http_response_t resp;
int rc = nostr_http_request(&req, &resp);
if (rc != NOSTR_SUCCESS) return rc;
if (status_out) *status_out = resp.status_code;
*body_out = resp.body;
free(resp.content_type);
free(resp.headers_raw);
return NOSTR_SUCCESS;
}
int nostr_http_post_json(const char* url,
const char* json_body,
int timeout_seconds,
char** body_out,
long* status_out) {
if (!url || !body_out) return NOSTR_ERROR_INVALID_INPUT;
*body_out = NULL;
if (status_out) *status_out = 0;
const char* headers[] = {
"Accept: application/json",
"Content-Type: application/json",
NULL
};
const char* payload = json_body ? json_body : "{}";
nostr_http_request_t req;
memset(&req, 0, sizeof(req));
req.method = "POST";
req.url = url;
req.headers = headers;
req.body = (const unsigned char*)payload;
req.body_len = strlen(payload);
req.timeout_seconds = timeout_seconds;
req.follow_redirects = 1;
req.max_redirects = 3;
nostr_http_response_t resp;
int rc = nostr_http_request(&req, &resp);
if (rc != NOSTR_SUCCESS) return rc;
if (status_out) *status_out = resp.status_code;
*body_out = resp.body;
free(resp.content_type);
free(resp.headers_raw);
return NOSTR_SUCCESS;
}
+55
View File
@@ -0,0 +1,55 @@
/*
* NOSTR Core Library - Shared HTTP Client
*/
#ifndef NOSTR_HTTP_H
#define NOSTR_HTTP_H
#include "nostr_common.h"
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
char* body;
size_t body_len;
long status_code;
char* content_type;
char* headers_raw;
int truncated;
} nostr_http_response_t;
typedef struct {
const char* method;
const char* url;
const char** headers;
const unsigned char* body;
size_t body_len;
int timeout_seconds;
size_t max_response_bytes;
int follow_redirects;
int max_redirects;
const char* user_agent;
int capture_headers;
} nostr_http_request_t;
void nostr_http_set_ca_bundle(const char* ca_bundle_path);
const char* nostr_http_detect_ca_bundle(void);
int nostr_http_request(const nostr_http_request_t* req, nostr_http_response_t* resp);
void nostr_http_response_free(nostr_http_response_t* resp);
int nostr_http_get(const char* url, int timeout_seconds, char** body_out, long* status_out);
int nostr_http_post_json(const char* url,
const char* json_body,
int timeout_seconds,
char** body_out,
long* status_out);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_HTTP_H */
+20
View File
@@ -0,0 +1,20 @@
#ifndef NOSTR_PLATFORM_H
#define NOSTR_PLATFORM_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* Fill buffer with cryptographically secure random bytes.
* Returns 0 on success, -1 on failure.
*/
int nostr_platform_random(unsigned char *buf, size_t len);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_PLATFORM_H */
+12 -6
View File
@@ -433,32 +433,38 @@ int nostr_sha256_final(nostr_sha256_ctx_t* ctx, unsigned char* hash) {
}
int nostr_sha256_file_stream(const char* filename, unsigned char* hash) {
#ifndef NOSTR_NO_FILESYSTEM
if (!filename || !hash) return -1;
FILE* file = fopen(filename, "rb");
if (!file) return -1;
nostr_sha256_ctx_t ctx;
if (nostr_sha256_init(&ctx) != 0) {
fclose(file);
return -1;
}
// Process file in 4KB chunks for memory efficiency
unsigned char buffer[4096];
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) {
if (nostr_sha256_update(&ctx, buffer, bytes_read) != 0) {
fclose(file);
return -1;
}
}
fclose(file);
// Finalize and return result
return nostr_sha256_final(&ctx, hash);
#else
(void)filename;
(void)hash;
return -1;
#endif
}
// =============================================================================
+312
View File
@@ -0,0 +1,312 @@
#include "../../nostr_core/nostr_http.h"
#include "esp_http_client.h"
#include <stdlib.h>
#include <string.h>
#include <strings.h>
typedef struct {
char* data;
size_t len;
size_t cap;
size_t max_bytes;
int truncated;
} nostr_http_buffer_t;
static char g_ca_bundle_path[512] = {0};
static char* nostr_http_strdup_local(const char* s) {
if (!s) return NULL;
size_t n = strlen(s);
char* out = (char*)malloc(n + 1U);
if (!out) return NULL;
memcpy(out, s, n + 1U);
return out;
}
static int append_bytes(nostr_http_buffer_t* b, const void* src, size_t n) {
if (!b || !src || n == 0) return 1;
if (b->max_bytes > 0 && b->len >= b->max_bytes) {
b->truncated = 1;
return 1;
}
size_t to_copy = n;
if (b->max_bytes > 0) {
size_t remaining = b->max_bytes - b->len;
if (to_copy > remaining) {
to_copy = remaining;
b->truncated = 1;
}
}
if (b->len + to_copy + 1U > b->cap) {
size_t new_cap = b->cap == 0 ? 1024U : b->cap;
while (new_cap < b->len + to_copy + 1U) {
new_cap *= 2U;
}
char* p = (char*)realloc(b->data, new_cap);
if (!p) return 0;
b->data = p;
b->cap = new_cap;
}
memcpy(b->data + b->len, src, to_copy);
b->len += to_copy;
b->data[b->len] = '\0';
return 1;
}
typedef struct {
nostr_http_buffer_t* body;
nostr_http_buffer_t* headers;
int capture_headers;
} nostr_http_event_ctx_t;
static esp_err_t http_event_handler(esp_http_client_event_t* evt) {
if (!evt || !evt->user_data) return ESP_OK;
nostr_http_event_ctx_t* ctx = (nostr_http_event_ctx_t*)evt->user_data;
switch (evt->event_id) {
case HTTP_EVENT_ON_DATA:
if (evt->data && evt->data_len > 0 && ctx->body) {
if (!append_bytes(ctx->body, evt->data, (size_t)evt->data_len)) {
return ESP_FAIL;
}
}
break;
case HTTP_EVENT_ON_HEADER:
if (ctx->capture_headers && ctx->headers && evt->header_key && evt->header_value) {
if (!append_bytes(ctx->headers, evt->header_key, strlen(evt->header_key)) ||
!append_bytes(ctx->headers, ": ", 2) ||
!append_bytes(ctx->headers, evt->header_value, strlen(evt->header_value)) ||
!append_bytes(ctx->headers, "\r\n", 2)) {
return ESP_FAIL;
}
}
break;
default:
break;
}
return ESP_OK;
}
void nostr_http_set_ca_bundle(const char* ca_bundle_path) {
if (!ca_bundle_path || ca_bundle_path[0] == '\0') {
g_ca_bundle_path[0] = '\0';
return;
}
strncpy(g_ca_bundle_path, ca_bundle_path, sizeof(g_ca_bundle_path) - 1);
g_ca_bundle_path[sizeof(g_ca_bundle_path) - 1] = '\0';
}
const char* nostr_http_detect_ca_bundle(void) {
if (g_ca_bundle_path[0] != '\0') {
return g_ca_bundle_path;
}
return NULL;
}
int nostr_http_request(const nostr_http_request_t* req, nostr_http_response_t* resp) {
if (!req || !req->url || !resp) return NOSTR_ERROR_INVALID_INPUT;
memset(resp, 0, sizeof(*resp));
const char* method = (req->method && req->method[0] != '\0') ? req->method : "GET";
int timeout_ms = (req->timeout_seconds > 0 ? req->timeout_seconds : 30) * 1000;
nostr_http_buffer_t body = {0};
body.max_bytes = req->max_response_bytes;
nostr_http_buffer_t headers = {0};
nostr_http_event_ctx_t evt_ctx = {
.body = &body,
.headers = &headers,
.capture_headers = req->capture_headers
};
esp_http_client_config_t cfg = {
.url = req->url,
.timeout_ms = timeout_ms,
.event_handler = http_event_handler,
.user_data = &evt_ctx,
.disable_auto_redirect = req->follow_redirects ? false : true,
.max_redirection_count = req->max_redirects > 0 ? req->max_redirects : 3,
};
esp_http_client_handle_t client = esp_http_client_init(&cfg);
if (!client) {
return NOSTR_ERROR_NETWORK_FAILED;
}
if (req->user_agent && req->user_agent[0] != '\0') {
esp_http_client_set_header(client, "User-Agent", req->user_agent);
} else {
esp_http_client_set_header(client, "User-Agent", "nostr-core/1.0");
}
if (req->headers) {
for (const char** h = req->headers; *h; h++) {
const char* line = *h;
if (!line || line[0] == '\0') continue;
const char* colon = strchr(line, ':');
if (!colon) continue;
size_t key_len = (size_t)(colon - line);
if (key_len == 0) continue;
char* key = (char*)malloc(key_len + 1U);
if (!key) {
esp_http_client_cleanup(client);
free(body.data);
free(headers.data);
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(key, line, key_len);
key[key_len] = '\0';
const char* value = colon + 1;
while (*value == ' ') value++;
esp_http_client_set_header(client, key, value);
free(key);
}
}
if (strcasecmp(method, "GET") == 0) {
esp_http_client_set_method(client, HTTP_METHOD_GET);
} else if (strcasecmp(method, "POST") == 0) {
esp_http_client_set_method(client, HTTP_METHOD_POST);
if (req->body && req->body_len > 0) {
esp_http_client_set_post_field(client, (const char*)req->body, (int)req->body_len);
} else {
esp_http_client_set_post_field(client, "", 0);
}
} else if (strcasecmp(method, "HEAD") == 0) {
esp_http_client_set_method(client, HTTP_METHOD_HEAD);
} else if (strcasecmp(method, "PUT") == 0) {
esp_http_client_set_method(client, HTTP_METHOD_PUT);
if (req->body && req->body_len > 0) {
esp_http_client_set_post_field(client, (const char*)req->body, (int)req->body_len);
}
} else if (strcasecmp(method, "DELETE") == 0) {
esp_http_client_set_method(client, HTTP_METHOD_DELETE);
} else {
esp_http_client_cleanup(client);
return NOSTR_ERROR_INVALID_INPUT;
}
esp_err_t err = esp_http_client_perform(client);
if (err != ESP_OK) {
esp_http_client_cleanup(client);
free(body.data);
free(headers.data);
return NOSTR_ERROR_NETWORK_FAILED;
}
resp->status_code = esp_http_client_get_status_code(client);
resp->body = body.data ? body.data : nostr_http_strdup_local("");
resp->body_len = body.data ? body.len : 0;
resp->headers_raw = headers.data;
resp->truncated = body.truncated;
char* ct = NULL;
char* ct_raw = NULL;
if (esp_http_client_get_header(client, "Content-Type", &ct_raw) == ESP_OK && ct_raw && ct_raw[0] != '\0') {
ct = nostr_http_strdup_local(ct_raw);
}
resp->content_type = ct;
esp_http_client_cleanup(client);
if (!resp->body) {
free(resp->headers_raw);
free(resp->content_type);
memset(resp, 0, sizeof(*resp));
return NOSTR_ERROR_MEMORY_FAILED;
}
return NOSTR_SUCCESS;
}
void nostr_http_response_free(nostr_http_response_t* resp) {
if (!resp) return;
free(resp->body);
free(resp->content_type);
free(resp->headers_raw);
memset(resp, 0, sizeof(*resp));
}
int nostr_http_get(const char* url, int timeout_seconds, char** body_out, long* status_out) {
if (!url || !body_out) return NOSTR_ERROR_INVALID_INPUT;
*body_out = NULL;
if (status_out) *status_out = 0;
nostr_http_request_t req;
memset(&req, 0, sizeof(req));
req.method = "GET";
req.url = url;
req.timeout_seconds = timeout_seconds;
req.follow_redirects = 1;
req.max_redirects = 3;
nostr_http_response_t resp;
int rc = nostr_http_request(&req, &resp);
if (rc != NOSTR_SUCCESS) return rc;
if (status_out) *status_out = resp.status_code;
*body_out = resp.body;
free(resp.content_type);
free(resp.headers_raw);
return NOSTR_SUCCESS;
}
int nostr_http_post_json(const char* url,
const char* json_body,
int timeout_seconds,
char** body_out,
long* status_out) {
if (!url || !body_out) return NOSTR_ERROR_INVALID_INPUT;
*body_out = NULL;
if (status_out) *status_out = 0;
const char* headers[] = {
"Accept: application/json",
"Content-Type: application/json",
NULL
};
const char* payload = json_body ? json_body : "{}";
nostr_http_request_t req;
memset(&req, 0, sizeof(req));
req.method = "POST";
req.url = url;
req.headers = headers;
req.body = (const unsigned char*)payload;
req.body_len = strlen(payload);
req.timeout_seconds = timeout_seconds;
req.follow_redirects = 1;
req.max_redirects = 3;
nostr_http_response_t resp;
int rc = nostr_http_request(&req, &resp);
if (rc != NOSTR_SUCCESS) return rc;
if (status_out) *status_out = resp.status_code;
*body_out = resp.body;
free(resp.content_type);
free(resp.headers_raw);
return NOSTR_SUCCESS;
}
+12
View File
@@ -0,0 +1,12 @@
#include "../../nostr_core/nostr_platform.h"
#include "esp_random.h"
int nostr_platform_random(unsigned char *buf, size_t len) {
if (buf == NULL || len == 0) {
return -1;
}
esp_fill_random(buf, len);
return 0;
}
+457
View File
@@ -0,0 +1,457 @@
#include "../../nostr_websocket/nostr_websocket_tls.h"
#include "esp_crt_bundle.h"
#include "esp_err.h"
#include "esp_log.h"
#include <ctype.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <esp_transport.h>
#include <esp_transport_ssl.h>
#include <esp_transport_tcp.h>
#include <esp_transport_ws.h>
typedef struct {
char* host;
int port;
char* path;
int use_tls;
} ws_url_parts_t;
struct nostr_ws_client {
esp_transport_list_handle_t list;
esp_transport_handle_t transport;
nostr_ws_state_t state;
int timeout_ms;
char* host;
int port;
char* path;
};
static void ws_free_url_parts(ws_url_parts_t* p) {
if (!p) return;
free(p->host);
free(p->path);
p->host = NULL;
p->path = NULL;
p->port = 0;
p->use_tls = 0;
}
static int ws_parse_url(const char* url, ws_url_parts_t* out) {
if (!url || !out) return -1;
memset(out, 0, sizeof(*out));
const char* p = NULL;
if (strncmp(url, "ws://", 5) == 0) {
out->use_tls = 0;
out->port = 80;
p = url + 5;
} else if (strncmp(url, "wss://", 6) == 0) {
out->use_tls = 1;
out->port = 443;
p = url + 6;
} else {
return -1;
}
const char* slash = strchr(p, '/');
const char* host_end = slash ? slash : (p + strlen(p));
const char* colon = NULL;
for (const char* it = p; it < host_end; ++it) {
if (*it == ':') {
colon = it;
break;
}
}
size_t host_len;
if (colon) {
host_len = (size_t)(colon - p);
char portbuf[8] = {0};
size_t port_len = (size_t)(host_end - colon - 1);
if (port_len == 0 || port_len >= sizeof(portbuf)) return -1;
memcpy(portbuf, colon + 1, port_len);
for (size_t i = 0; i < port_len; i++) {
if (!isdigit((unsigned char)portbuf[i])) return -1;
}
out->port = atoi(portbuf);
if (out->port <= 0 || out->port > 65535) return -1;
} else {
host_len = (size_t)(host_end - p);
}
if (host_len == 0) return -1;
out->host = (char*)malloc(host_len + 1U);
if (!out->host) return -1;
memcpy(out->host, p, host_len);
out->host[host_len] = '\0';
out->path = slash ? strdup(slash) : strdup("/");
if (!out->path) {
ws_free_url_parts(out);
return -1;
}
return 0;
}
static void ws_client_destroy(struct nostr_ws_client* c) {
if (!c) return;
if (c->transport) {
esp_transport_close(c->transport);
c->transport = NULL;
}
if (c->list) {
esp_transport_list_destroy(c->list);
c->list = NULL;
}
free(c->host);
free(c->path);
free(c);
}
static esp_transport_handle_t ws_build_transport(const ws_url_parts_t* up,
esp_transport_list_handle_t list) {
if (!up || !list) return NULL;
if (up->use_tls) {
esp_transport_handle_t ssl = esp_transport_ssl_init();
if (!ssl) return NULL;
esp_transport_ssl_crt_bundle_attach(ssl, esp_crt_bundle_attach);
esp_transport_set_default_port(ssl, 443);
if (esp_transport_list_add(list, ssl, "ssl") != ESP_OK) {
esp_transport_destroy(ssl);
return NULL;
}
esp_transport_handle_t wss = esp_transport_ws_init(ssl);
if (!wss) return NULL;
esp_transport_set_default_port(wss, up->port);
esp_transport_ws_set_path(wss, up->path);
if (esp_transport_ws_set_user_agent(wss, "nostr_core_lib/esp32") != ESP_OK) {
esp_transport_destroy(wss);
return NULL;
}
if (esp_transport_list_add(list, wss, "wss") != ESP_OK) {
esp_transport_destroy(wss);
return NULL;
}
return wss;
}
esp_transport_handle_t tcp = esp_transport_tcp_init();
if (!tcp) return NULL;
esp_transport_set_default_port(tcp, 80);
if (esp_transport_list_add(list, tcp, "tcp") != ESP_OK) {
esp_transport_destroy(tcp);
return NULL;
}
esp_transport_handle_t ws = esp_transport_ws_init(tcp);
if (!ws) return NULL;
esp_transport_set_default_port(ws, up->port);
esp_transport_ws_set_path(ws, up->path);
if (esp_transport_ws_set_user_agent(ws, "nostr_core_lib/esp32") != ESP_OK) {
esp_transport_destroy(ws);
return NULL;
}
if (esp_transport_list_add(list, ws, "ws") != ESP_OK) {
esp_transport_destroy(ws);
return NULL;
}
return ws;
}
nostr_ws_client_t* nostr_ws_connect(const char* url) {
if (!url) return NULL;
ws_url_parts_t up;
if (ws_parse_url(url, &up) != 0) return NULL;
struct nostr_ws_client* c = (struct nostr_ws_client*)calloc(1, sizeof(struct nostr_ws_client));
if (!c) {
ws_free_url_parts(&up);
return NULL;
}
c->list = esp_transport_list_init();
if (!c->list) {
ws_free_url_parts(&up);
free(c);
return NULL;
}
c->transport = ws_build_transport(&up, c->list);
if (!c->transport) {
ws_free_url_parts(&up);
ws_client_destroy(c);
return NULL;
}
c->timeout_ms = 30000;
c->state = NOSTR_WS_CONNECTING;
c->host = up.host;
c->port = up.port;
c->path = up.path;
up.host = NULL;
up.path = NULL;
int cr = esp_transport_connect(c->transport, c->host, c->port, c->timeout_ms);
if (cr < 0) {
c->state = NOSTR_WS_ERROR;
ws_client_destroy(c);
ws_free_url_parts(&up);
return NULL;
}
int hs = esp_transport_ws_get_upgrade_request_status(c->transport);
if (hs != 101) {
c->state = NOSTR_WS_ERROR;
ws_client_destroy(c);
ws_free_url_parts(&up);
return NULL;
}
c->state = NOSTR_WS_CONNECTED;
ws_free_url_parts(&up);
return c;
}
int nostr_ws_close(nostr_ws_client_t* client) {
if (!client) return NOSTR_WS_ERROR_INVALID;
if (client->state == NOSTR_WS_CONNECTED) {
(void)esp_transport_ws_send_raw(client->transport,
(ws_transport_opcodes_t)(WS_TRANSPORT_OPCODES_CLOSE | WS_TRANSPORT_OPCODES_FIN),
NULL,
0,
client->timeout_ms);
}
client->state = NOSTR_WS_CLOSED;
ws_client_destroy(client);
return NOSTR_WS_SUCCESS;
}
nostr_ws_state_t nostr_ws_get_state(nostr_ws_client_t* client) {
if (!client) return NOSTR_WS_ERROR;
return client->state;
}
int nostr_ws_send_text(nostr_ws_client_t* client, const char* message) {
if (!client || !message || client->state != NOSTR_WS_CONNECTED) {
return NOSTR_WS_ERROR_INVALID;
}
int len = (int)strlen(message);
int wr = esp_transport_ws_send_raw(client->transport,
(ws_transport_opcodes_t)(WS_TRANSPORT_OPCODES_TEXT | WS_TRANSPORT_OPCODES_FIN),
message,
len,
client->timeout_ms);
return (wr == len) ? NOSTR_WS_SUCCESS : NOSTR_WS_ERROR_NETWORK;
}
int nostr_ws_receive(nostr_ws_client_t* client, char* buffer, size_t buffer_size, int timeout_ms) {
if (!client || !buffer || buffer_size == 0) return NOSTR_WS_ERROR_INVALID;
if (client->state != NOSTR_WS_CONNECTED && client->state != NOSTR_WS_CLOSING) {
return NOSTR_WS_ERROR_INVALID;
}
int tmo = (timeout_ms > 0) ? timeout_ms : client->timeout_ms;
int n = esp_transport_read(client->transport, buffer, (int)buffer_size - 1, tmo);
if (n < 0) {
return NOSTR_WS_ERROR_NETWORK;
}
if (n == 0) {
/* timeout or internally-handled control frame; not a hard network failure */
buffer[0] = '\0';
return 0;
}
ws_transport_opcodes_t opcode = esp_transport_ws_get_read_opcode(client->transport);
if (opcode == WS_TRANSPORT_OPCODES_TEXT || opcode == WS_TRANSPORT_OPCODES_BINARY) {
buffer[n] = '\0';
return n;
}
if (opcode == WS_TRANSPORT_OPCODES_PING) {
(void)esp_transport_ws_send_raw(client->transport,
(ws_transport_opcodes_t)(WS_TRANSPORT_OPCODES_PONG | WS_TRANSPORT_OPCODES_FIN),
buffer,
n,
tmo);
return nostr_ws_receive(client, buffer, buffer_size, timeout_ms);
}
if (opcode == WS_TRANSPORT_OPCODES_PONG) {
buffer[n] = '\0';
return n;
}
if (opcode == WS_TRANSPORT_OPCODES_CLOSE) {
client->state = NOSTR_WS_CLOSING;
buffer[0] = '\0';
return 0;
}
return NOSTR_WS_ERROR_PROTOCOL;
}
int nostr_ws_ping(nostr_ws_client_t* client) {
if (!client || client->state != NOSTR_WS_CONNECTED) {
return NOSTR_WS_ERROR_INVALID;
}
int wr = esp_transport_ws_send_raw(client->transport,
(ws_transport_opcodes_t)(WS_TRANSPORT_OPCODES_PING | WS_TRANSPORT_OPCODES_FIN),
"ping",
4,
client->timeout_ms);
return (wr == 4) ? NOSTR_WS_SUCCESS : NOSTR_WS_ERROR_NETWORK;
}
int nostr_ws_set_timeout(nostr_ws_client_t* client, int timeout_ms) {
if (!client || timeout_ms < 0) return NOSTR_WS_ERROR_INVALID;
client->timeout_ms = timeout_ms;
return NOSTR_WS_SUCCESS;
}
int nostr_relay_send_req(nostr_ws_client_t* client, const char* subscription_id, cJSON* filters) {
if (!client || !subscription_id || !filters) {
return NOSTR_WS_ERROR_INVALID;
}
cJSON* req_array = cJSON_CreateArray();
cJSON_AddItemToArray(req_array, cJSON_CreateString("REQ"));
cJSON_AddItemToArray(req_array, cJSON_CreateString(subscription_id));
if (cJSON_IsArray(filters)) {
cJSON* filter;
cJSON_ArrayForEach(filter, filters) {
cJSON_AddItemToArray(req_array, cJSON_Duplicate(filter, 1));
}
} else {
cJSON_AddItemToArray(req_array, cJSON_Duplicate(filters, 1));
}
char* req_string = cJSON_PrintUnformatted(req_array);
if (!req_string) {
cJSON_Delete(req_array);
return NOSTR_WS_ERROR_MEMORY;
}
int result = nostr_ws_send_text(client, req_string);
free(req_string);
cJSON_Delete(req_array);
return result;
}
int nostr_relay_send_event(nostr_ws_client_t* client, cJSON* event) {
if (!client || !event) {
return NOSTR_WS_ERROR_INVALID;
}
cJSON* event_array = cJSON_CreateArray();
cJSON_AddItemToArray(event_array, cJSON_CreateString("EVENT"));
cJSON_AddItemToArray(event_array, cJSON_Duplicate(event, 1));
char* event_string = cJSON_PrintUnformatted(event_array);
if (!event_string) {
cJSON_Delete(event_array);
return NOSTR_WS_ERROR_MEMORY;
}
int result = nostr_ws_send_text(client, event_string);
free(event_string);
cJSON_Delete(event_array);
return result;
}
int nostr_relay_send_close(nostr_ws_client_t* client, const char* subscription_id) {
if (!client || !subscription_id) {
return NOSTR_WS_ERROR_INVALID;
}
cJSON* close_array = cJSON_CreateArray();
cJSON_AddItemToArray(close_array, cJSON_CreateString("CLOSE"));
cJSON_AddItemToArray(close_array, cJSON_CreateString(subscription_id));
char* close_string = cJSON_PrintUnformatted(close_array);
if (!close_string) {
cJSON_Delete(close_array);
return NOSTR_WS_ERROR_MEMORY;
}
int result = nostr_ws_send_text(client, close_string);
free(close_string);
cJSON_Delete(close_array);
return result;
}
int nostr_parse_relay_message(const char* message, char** message_type, cJSON** parsed_json) {
if (!message || !message_type || !parsed_json) {
return NOSTR_WS_ERROR_INVALID;
}
*message_type = NULL;
*parsed_json = NULL;
cJSON* json = cJSON_Parse(message);
if (!json || !cJSON_IsArray(json)) {
cJSON_Delete(json);
return NOSTR_WS_ERROR_PROTOCOL;
}
cJSON* type_item = cJSON_GetArrayItem(json, 0);
if (!type_item || !cJSON_IsString(type_item)) {
cJSON_Delete(json);
return NOSTR_WS_ERROR_PROTOCOL;
}
*message_type = strdup(type_item->valuestring);
if (!*message_type) {
cJSON_Delete(json);
return NOSTR_WS_ERROR_MEMORY;
}
*parsed_json = json;
return NOSTR_WS_SUCCESS;
}
const char* nostr_ws_strerror(int error_code) {
switch (error_code) {
case NOSTR_WS_SUCCESS: return "Success";
case NOSTR_WS_ERROR_INVALID: return "Invalid parameter";
case NOSTR_WS_ERROR_NETWORK: return "Network error";
case NOSTR_WS_ERROR_PROTOCOL: return "Protocol error";
case NOSTR_WS_ERROR_MEMORY: return "Memory allocation error";
case NOSTR_WS_ERROR_TLS: return "TLS error";
default: return "Unknown error";
}
}
+28
View File
@@ -0,0 +1,28 @@
#include "../nostr_core/nostr_platform.h"
#include <fcntl.h>
#include <unistd.h>
int nostr_platform_random(unsigned char *buf, size_t len) {
if (buf == NULL || len == 0) {
return -1;
}
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) {
return -1;
}
size_t total = 0;
while (total < len) {
ssize_t n = read(fd, buf + total, len - total);
if (n <= 0) {
close(fd);
return -1;
}
total += (size_t)n;
}
close(fd);
return 0;
}
+213
View File
@@ -0,0 +1,213 @@
/*
* Blossom Live Client Integration Test
*
* Uploads a text file to a Blossom server, downloads it back,
* and verifies the round-trip content.
*
* Required env:
* BLOSSOM_TEST_PRIVKEY_HEX = 64-char hex private key
*
* Optional env:
* BLOSSOM_TEST_SERVER = Blossom base URL (default: https://blossom.laantungir.net)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "../nostr_core/nostr_core.h"
static int write_text_file(const char* path, const char* text) {
if (!path || !text) return -1;
FILE* fp = fopen(path, "wb");
if (!fp) return -1;
size_t len = strlen(text);
size_t n = fwrite(text, 1, len, fp);
fclose(fp);
return (n == len) ? 0 : -1;
}
static int read_text_file(const char* path, char** out_text, size_t* out_len) {
if (!path || !out_text || !out_len) return -1;
*out_text = NULL;
*out_len = 0;
FILE* fp = fopen(path, "rb");
if (!fp) return -1;
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return -1;
}
long sz = ftell(fp);
if (sz < 0) {
fclose(fp);
return -1;
}
if (fseek(fp, 0, SEEK_SET) != 0) {
fclose(fp);
return -1;
}
char* buf = (char*)malloc((size_t)sz + 1U);
if (!buf) {
fclose(fp);
return -1;
}
size_t n = fread(buf, 1, (size_t)sz, fp);
fclose(fp);
if (n != (size_t)sz) {
free(buf);
return -1;
}
buf[n] = '\0';
*out_text = buf;
*out_len = n;
return 0;
}
static int load_private_key_from_env(unsigned char out_privkey[32]) {
const char* hex = getenv("BLOSSOM_TEST_PRIVKEY_HEX");
if (!hex || hex[0] == '\0') {
printf("[SKIP] BLOSSOM_TEST_PRIVKEY_HEX not set\n");
return 1;
}
if (strlen(hex) != 64) {
printf("[SKIP] BLOSSOM_TEST_PRIVKEY_HEX must be 64 hex chars\n");
return 1;
}
if (nostr_hex_to_bytes(hex, out_privkey, 32) != 0) {
printf("[SKIP] BLOSSOM_TEST_PRIVKEY_HEX is not valid hex\n");
return 1;
}
return 0;
}
int main(void) {
const char* server = getenv("BLOSSOM_TEST_SERVER");
if (!server || server[0] == '\0') {
server = "https://blossom.laantungir.net";
}
printf("Blossom Live Client Integration Test\n");
printf("====================================\n");
printf("Server: %s\n", server);
const char* ca_bundle = nostr_http_detect_ca_bundle();
if (ca_bundle && ca_bundle[0] != '\0') {
nostr_http_set_ca_bundle(ca_bundle);
}
unsigned char private_key[32] = {0};
int key_rc = load_private_key_from_env(private_key);
if (key_rc != 0) {
return (key_rc == 1) ? 0 : 1;
}
char input_text[1024];
time_t now = time(NULL);
int nw = snprintf(input_text,
sizeof(input_text),
"Didactyl Blossom live test\n"
"timestamp=%ld\n"
"server=%s\n"
"message=Round-trip file upload/download validation.\n",
(long)now,
server);
if (nw < 0 || (size_t)nw >= sizeof(input_text)) {
printf("[FAIL] Unable to create input text payload\n");
return 1;
}
const char* input_path = "tests/.tmp_blossom_input.txt";
const char* output_path = "tests/.tmp_blossom_output.txt";
if (write_text_file(input_path, input_text) != 0) {
printf("[FAIL] Unable to write input file: %s\n", input_path);
return 1;
}
blossom_blob_descriptor_t uploaded;
memset(&uploaded, 0, sizeof(uploaded));
int rc = blossom_upload_file(server,
input_path,
"text/plain; charset=utf-8",
private_key,
30,
&uploaded);
if (rc != NOSTR_SUCCESS) {
printf("[FAIL] blossom_upload_file rc=%d\n", rc);
remove(input_path);
return 1;
}
if (uploaded.sha256[0] == '\0') {
printf("[FAIL] Upload succeeded but returned empty sha256\n");
remove(input_path);
return 1;
}
printf("[INFO] Uploaded sha256: %s\n", uploaded.sha256);
printf("[INFO] Uploaded url: %s\n", uploaded.url);
blossom_blob_descriptor_t downloaded;
memset(&downloaded, 0, sizeof(downloaded));
rc = blossom_download_to_file(server,
uploaded.sha256,
output_path,
30,
1024U * 1024U,
&downloaded);
if (rc != NOSTR_SUCCESS) {
printf("[FAIL] blossom_download_to_file rc=%d\n", rc);
remove(input_path);
return 1;
}
char* output_text = NULL;
size_t output_len = 0;
if (read_text_file(output_path, &output_text, &output_len) != 0) {
printf("[FAIL] Unable to read output file: %s\n", output_path);
remove(input_path);
remove(output_path);
return 1;
}
size_t input_len = strlen(input_text);
int same = (input_len == output_len) && (memcmp(input_text, output_text, input_len) == 0);
if (!same) {
printf("[FAIL] Round-trip mismatch: input_len=%zu output_len=%zu\n", input_len, output_len);
free(output_text);
remove(input_path);
remove(output_path);
return 1;
}
if (strcmp(uploaded.sha256, downloaded.sha256) != 0) {
printf("[FAIL] SHA mismatch: uploaded=%s downloaded=%s\n", uploaded.sha256, downloaded.sha256);
free(output_text);
remove(input_path);
remove(output_path);
return 1;
}
printf("[PASS] Upload/download round-trip verified\n");
printf("[PASS] sha256=%s\n", uploaded.sha256);
printf("[PASS] bytes=%zu\n", output_len);
free(output_text);
remove(input_path);
remove(output_path);
return 0;
}
+139
View File
@@ -0,0 +1,139 @@
/*
* blossom_client unit/integration tests using local mock Blossom server.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../nostr_core/nostr_core.h"
static int tests_run = 0;
static int tests_passed = 0;
#define TEST_ASSERT(cond, msg) do { \
tests_run++; \
if (cond) { tests_passed++; printf("✅ %s\n", msg); } \
else { printf("❌ %s\n", msg); } \
} while (0)
static const char* base_url(void) {
const char* env = getenv("MOCK_BLOSSOM_BASE");
return (env && env[0] != '\0') ? env : "http://127.0.0.1:18081";
}
static void test_auth_header(void) {
unsigned char priv[32];
memset(priv, 1, sizeof(priv));
char sha[65];
memset(sha, 'a', 64);
sha[64] = '\0';
char* hdr = blossom_create_auth_header(priv, "upload", sha, 300);
TEST_ASSERT(hdr != NULL, "blossom_create_auth_header returns non-null");
TEST_ASSERT(hdr && strncmp(hdr, "Nostr ", 6) == 0, "blossom_create_auth_header prefix is 'Nostr '");
free(hdr);
}
static void test_invalid_inputs(void) {
blossom_blob_descriptor_t d;
memset(&d, 0, sizeof(d));
int rc = blossom_upload(NULL, (const unsigned char*)"x", 1, NULL, NULL, NULL, 5, &d);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "blossom_upload rejects null server");
unsigned char* body = NULL;
size_t body_len = 0;
rc = blossom_download(base_url(), "not-a-sha", 5, 1024, &body, &body_len, NULL, 0);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "blossom_download rejects invalid sha");
rc = blossom_head(base_url(), "not-a-sha", 5, &d);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "blossom_head rejects invalid sha");
rc = blossom_delete(base_url(), "not-a-sha", (const unsigned char*)"x", 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "blossom_delete rejects invalid sha");
blossom_blob_descriptor_t* items = NULL;
int count = 0;
rc = blossom_list(NULL, "abc", 5, &items, &count);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "blossom_list rejects null server");
}
static void test_round_trip_with_mock(void) {
const char* server = base_url();
const unsigned char payload[] = "blossom-client-test-payload";
blossom_blob_descriptor_t uploaded;
memset(&uploaded, 0, sizeof(uploaded));
int rc = blossom_upload(server,
payload,
sizeof(payload) - 1,
"text/plain",
NULL,
NULL,
5,
&uploaded);
TEST_ASSERT(rc == NOSTR_SUCCESS, "blossom_upload mock success");
TEST_ASSERT(uploaded.sha256[0] != '\0', "blossom_upload returns sha256");
unsigned char* body = NULL;
size_t body_len = 0;
char content_type[128] = {0};
rc = blossom_download(server,
uploaded.sha256,
5,
4096,
&body,
&body_len,
content_type,
sizeof(content_type));
TEST_ASSERT(rc == NOSTR_SUCCESS, "blossom_download mock success");
TEST_ASSERT(body && body_len == sizeof(payload) - 1, "blossom_download length matches");
TEST_ASSERT(body && memcmp(body, payload, body_len) == 0, "blossom_download payload matches");
free(body);
blossom_blob_descriptor_t head;
memset(&head, 0, sizeof(head));
rc = blossom_head(server, uploaded.sha256, 5, &head);
TEST_ASSERT(rc == NOSTR_SUCCESS, "blossom_head mock success");
TEST_ASSERT(head.size == (long)(sizeof(payload) - 1), "blossom_head size matches");
blossom_blob_descriptor_t* list = NULL;
int list_count = 0;
rc = blossom_list(server, "dummy_pubkey", 5, &list, &list_count);
TEST_ASSERT(rc == NOSTR_SUCCESS, "blossom_list mock success");
TEST_ASSERT(list_count >= 1, "blossom_list returns at least one blob");
free(list);
unsigned char fake_priv[32];
memset(fake_priv, 2, sizeof(fake_priv));
rc = blossom_delete(server, uploaded.sha256, fake_priv, 5);
TEST_ASSERT(rc == NOSTR_SUCCESS, "blossom_delete mock success");
memset(&head, 0, sizeof(head));
rc = blossom_head(server, uploaded.sha256, 5, &head);
TEST_ASSERT(rc == NOSTR_ERROR_NETWORK_FAILED, "blossom_head after delete reports missing blob");
}
int main(void) {
printf("blossom_client Tests\n");
printf("====================\n");
printf("Base URL: %s\n", base_url());
if (nostr_init() != NOSTR_SUCCESS) {
printf("❌ nostr_init failed\n");
return 1;
}
test_auth_header();
test_invalid_inputs();
test_round_trip_with_mock();
nostr_cleanup();
printf("\nSummary: %d/%d passed\n", tests_passed, tests_run);
return (tests_passed == tests_run) ? 0 : 1;
}
+67
View File
@@ -0,0 +1,67 @@
/*
* Blossom mock integration error-path tests.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../nostr_core/blossom_client.h"
#include "../nostr_core/nostr_common.h"
static int tests_run = 0;
static int tests_passed = 0;
#define TEST_ASSERT(cond, msg) do { \
tests_run++; \
if (cond) { tests_passed++; printf("✅ %s\n", msg); } \
else { printf("❌ %s\n", msg); } \
} while (0)
static const char* base_url(void) {
const char* env = getenv("MOCK_BLOSSOM_BASE");
return (env && env[0] != '\0') ? env : "http://127.0.0.1:18081";
}
static void make_error_server(char* out, size_t out_size, int status_code) {
snprintf(out, out_size, "%s/blossom/error/%d", base_url(), status_code);
}
static void test_forced_error_paths(void) {
char server[512];
char sha[65];
memset(sha, 'a', 64);
sha[64] = '\0';
unsigned char* body = NULL;
size_t body_len = 0;
make_error_server(server, sizeof(server), 404);
int rc = blossom_download(server, sha, 5, 1024, &body, &body_len, NULL, 0);
TEST_ASSERT(rc == NOSTR_ERROR_NETWORK_FAILED, "blossom_download handles forced 404");
make_error_server(server, sizeof(server), 401);
blossom_blob_descriptor_t d;
memset(&d, 0, sizeof(d));
rc = blossom_upload(server, (const unsigned char*)"x", 1, "text/plain", NULL, NULL, 5, &d);
TEST_ASSERT(rc == NOSTR_ERROR_NETWORK_FAILED, "blossom_upload handles forced 401");
make_error_server(server, sizeof(server), 413);
rc = blossom_upload(server, (const unsigned char*)"x", 1, "text/plain", NULL, NULL, 5, &d);
TEST_ASSERT(rc == NOSTR_ERROR_NETWORK_FAILED, "blossom_upload handles forced 413");
make_error_server(server, sizeof(server), 500);
rc = blossom_head(server, sha, 5, &d);
TEST_ASSERT(rc == NOSTR_ERROR_NETWORK_FAILED, "blossom_head handles forced 500");
}
int main(void) {
printf("blossom mock error-path tests\n");
printf("=============================\n");
printf("Base URL: %s\n", base_url());
test_forced_error_paths();
printf("\nSummary: %d/%d passed\n", tests_passed, tests_run);
return (tests_passed == tests_run) ? 0 : 1;
}
+243
View File
@@ -0,0 +1,243 @@
/*
* ChaCha20-Poly1305 / Poly1305 Test Suite - RFC 8439 vectors
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
int nostr_poly1305_mac(const unsigned char key[32], const unsigned char *msg,
size_t msg_len, unsigned char tag[16]);
int nostr_chacha20poly1305_encrypt(const unsigned char key[32],
const unsigned char nonce[12],
const unsigned char *aad, size_t aad_len,
const unsigned char *plaintext, size_t pt_len,
unsigned char *ciphertext,
unsigned char tag[16]);
int nostr_chacha20poly1305_decrypt(const unsigned char key[32],
const unsigned char nonce[12],
const unsigned char *aad, size_t aad_len,
const unsigned char *ciphertext, size_t ct_len,
const unsigned char tag[16],
unsigned char *plaintext);
static int hex_to_bytes(const char *hex, uint8_t *out, size_t out_len) {
size_t i;
if (!hex || !out) return -1;
for (i = 0; i < out_len; i++) {
if (sscanf(hex + (i * 2), "%2hhx", &out[i]) != 1) {
return -1;
}
}
return 0;
}
static int bytes_equal(const uint8_t *a, const uint8_t *b, size_t len) {
return memcmp(a, b, len) == 0;
}
static void print_hex(const uint8_t *buf, size_t len) {
size_t i;
for (i = 0; i < len; i++) {
printf("%02x", buf[i]);
}
}
static int test_poly1305_rfc8439_2_5_2(void) {
const char *key_hex =
"85d6be7857556d337f4452fe42d506a8"
"0103808afb0db2fd4abff6af4149f51b";
const char *msg = "Cryptographic Forum Research Group";
const char *expected_tag_hex = "a8061dc1305136c6c22b8baf0c0127a9";
uint8_t key[32];
uint8_t tag[16];
uint8_t expected[16];
printf("=== Poly1305 RFC 8439 §2.5.2 ===\n");
if (hex_to_bytes(key_hex, key, sizeof(key)) != 0 ||
hex_to_bytes(expected_tag_hex, expected, sizeof(expected)) != 0) {
printf("❌ hex parse failed\n\n");
return 0;
}
if (nostr_poly1305_mac(key, (const unsigned char *)msg, strlen(msg), tag) != 0) {
printf("❌ nostr_poly1305_mac failed\n\n");
return 0;
}
if (!bytes_equal(tag, expected, sizeof(tag))) {
printf("❌ tag mismatch\nExpected: ");
print_hex(expected, sizeof(expected));
printf("\nGot: ");
print_hex(tag, sizeof(tag));
printf("\n\n");
return 0;
}
printf("✅ passed\n\n");
return 1;
}
static int test_aead_rfc8439_appendix_a_5(void) {
const char *key_hex =
"1c9240a5eb55d38af333888604f6b5f0"
"473917c1402b80099dca5cbc207075c0";
const char *nonce_hex = "000000000102030405060708";
const char *aad_hex = "f33388860000000000004e91";
const char *plaintext_hex =
"496e7465726e65742d4472616674732061726520647261667420646f63756d656e74732076616c696420"
"666f722061206d6178696d756d206f6620736978206d6f6e74687320616e64206d61792062652075706461"
"7465642c207265706c616365642c206f72206f62736f6c65746564206279206f7468657220646f63756d65"
"6e747320617420616e792074696d652e20497420697320696e617070726f70726961746520746f20757365"
"20496e7465726e65742d447261667473206173207265666572656e6365206d6174657269616c206f722074"
"6f2063697465207468656d206f74686572207468616e206173202fe2809c776f726b20696e2070726f6772"
"6573732e2fe2809d";
const char *expected_ct_hex =
"64a0861575861af460f062c79be643bd"
"5e805cfd345cf389f108670ac76c8cb2"
"4c6cfc18755d43eea09ee94e382d26b0"
"bdb7b73c321b0100d4f03b7f355894cf"
"332f830e710b97ce98c8a84abd0b9481"
"14ad176e008d33bd60f982b1ff37c855"
"9797a06ef4f0ef61c186324e2b350638"
"3606907b6a7c02b0f9f6157b53c867e4"
"b9166c767b804d46a59b5216cde7a4e9"
"9040c5a40433225ee282a1b0a06c523e"
"af4534d7f83fa1155b0047718cbc546a"
"0d072b04b3564eea1b422273f548271a"
"0bb2316053fa76991955ebd63159434e"
"cebb4e466dae5a1073a6727627097a10"
"49e617d91d361094fa68f0ff77987130"
"305beaba2eda04df997b714d6c6f2c29"
"a6ad5cb4022b02709b";
const char *expected_tag_hex = "eead9d67890cbb22392336fea1851f38";
uint8_t key[32], nonce[12], aad[12], expected_tag[16];
uint8_t plaintext[1024], ciphertext[1024], decrypted[1024], expected_ct[1024];
uint8_t tag[16];
size_t pt_len = strlen(plaintext_hex) / 2;
size_t aad_len = strlen(aad_hex) / 2;
size_t ct_len = strlen(expected_ct_hex) / 2;
printf("=== ChaCha20-Poly1305 RFC 8439 Appendix A.5 ===\n");
if (hex_to_bytes(key_hex, key, sizeof(key)) != 0 ||
hex_to_bytes(nonce_hex, nonce, sizeof(nonce)) != 0 ||
hex_to_bytes(aad_hex, aad, aad_len) != 0 ||
hex_to_bytes(plaintext_hex, plaintext, pt_len) != 0 ||
hex_to_bytes(expected_ct_hex, expected_ct, ct_len) != 0 ||
hex_to_bytes(expected_tag_hex, expected_tag, sizeof(expected_tag)) != 0) {
printf("❌ hex parse failed\n\n");
return 0;
}
if (nostr_chacha20poly1305_encrypt(key, nonce, aad, aad_len,
plaintext, pt_len, ciphertext, tag) != 0) {
printf("❌ encrypt failed\n\n");
return 0;
}
if (!bytes_equal(ciphertext, expected_ct, ct_len)) {
size_t i;
size_t mismatch_count = 0;
printf("❌ ciphertext mismatch\n");
for (i = 0; i < ct_len; i++) {
if (ciphertext[i] != expected_ct[i]) {
if (mismatch_count == 0) {
size_t start = (i > 8) ? (i - 8) : 0;
size_t end = (i + 8 < ct_len) ? (i + 8) : (ct_len - 1);
size_t j;
printf("first mismatch at byte %zu: got=%02x expected=%02x\n", i, ciphertext[i], expected_ct[i]);
printf("context (got): ");
for (j = start; j <= end; j++) printf("%02x", ciphertext[j]);
printf("\ncontext (expected): ");
for (j = start; j <= end; j++) printf("%02x", expected_ct[j]);
printf("\n");
}
mismatch_count++;
}
}
printf("total mismatched bytes: %zu\n\n", mismatch_count);
return 0;
}
if (!bytes_equal(tag, expected_tag, sizeof(tag))) {
printf("❌ tag mismatch\n\n");
return 0;
}
if (nostr_chacha20poly1305_decrypt(key, nonce, aad, aad_len,
ciphertext, ct_len, tag, decrypted) != 0) {
printf("❌ decrypt failed\n\n");
return 0;
}
if (!bytes_equal(decrypted, plaintext, pt_len)) {
printf("❌ decrypted plaintext mismatch\n\n");
return 0;
}
printf("✅ passed\n\n");
return 1;
}
static int test_aead_tamper_rejects(void) {
uint8_t key[32] = {0};
uint8_t nonce[12] = {0};
uint8_t aad[8] = {1,2,3,4,5,6,7,8};
uint8_t pt[32] = "hello chacha20-poly1305 world";
uint8_t ct[64] = {0};
uint8_t tag[16] = {0};
uint8_t out[64] = {0};
printf("=== AEAD tamper rejection ===\n");
if (nostr_chacha20poly1305_encrypt(key, nonce, aad, sizeof(aad), pt, strlen((char *)pt), ct, tag) != 0) {
printf("❌ encrypt failed\n\n");
return 0;
}
ct[0] ^= 0x01;
if (nostr_chacha20poly1305_decrypt(key, nonce, aad, sizeof(aad), ct, strlen((char *)pt), tag, out) == 0) {
printf("❌ tampered ciphertext accepted\n\n");
return 0;
}
ct[0] ^= 0x01;
tag[0] ^= 0x80;
if (nostr_chacha20poly1305_decrypt(key, nonce, aad, sizeof(aad), ct, strlen((char *)pt), tag, out) == 0) {
printf("❌ tampered tag accepted\n\n");
return 0;
}
tag[0] ^= 0x80;
aad[0] ^= 0x01;
if (nostr_chacha20poly1305_decrypt(key, nonce, aad, sizeof(aad), ct, strlen((char *)pt), tag, out) == 0) {
printf("❌ tampered AAD accepted\n\n");
return 0;
}
printf("✅ passed\n\n");
return 1;
}
int main(void) {
int pass = 1;
pass &= test_poly1305_rfc8439_2_5_2();
pass &= test_aead_rfc8439_appendix_a_5();
pass &= test_aead_tamper_rejects();
if (pass) {
printf("🎉 All ChaCha20-Poly1305/Poly1305 tests PASSED\n");
return 0;
}
printf("💥 Some tests FAILED\n");
return 1;
}
+222
View File
@@ -0,0 +1,222 @@
#!/usr/bin/env python3
import hashlib
import json
import sys
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse, parse_qs
HOST = "127.0.0.1"
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 18081
STORE = {} # sha256 -> {bytes, content_type, created}
def json_bytes(obj):
return json.dumps(obj).encode("utf-8")
class Handler(BaseHTTPRequestHandler):
server_version = "mock-blossom/0.1"
def _write_json(self, code, obj):
body = json_bytes(obj)
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _read_body(self):
length = int(self.headers.get("Content-Length", "0"))
return self.rfile.read(length) if length > 0 else b""
def _maybe_error_prefix(self):
# /blossom/error/<code>/...
parts = [p for p in self.path.split("?")[0].split("/") if p]
if len(parts) >= 3 and parts[0] == "blossom" and parts[1] == "error":
try:
code = int(parts[2])
return code, parts[3:]
except ValueError:
return None, None
return None, None
def do_HEAD(self):
parsed = urlparse(self.path)
path = parsed.path
code, remaining = self._maybe_error_prefix()
if code is not None:
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", "0")
self.end_headers()
return
if path.startswith("/http/head"):
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", "0")
self.send_header("X-Mock", "head-ok")
self.end_headers()
return
sha = path.lstrip("/")
item = STORE.get(sha)
if not item:
self.send_response(404)
self.send_header("Content-Length", "0")
self.end_headers()
return
self.send_response(200)
self.send_header("Content-Type", item["content_type"])
self.send_header("Content-Length", str(len(item["bytes"])))
self.end_headers()
def do_GET(self):
parsed = urlparse(self.path)
path = parsed.path
qs = parse_qs(parsed.query)
code, remaining = self._maybe_error_prefix()
if code is not None:
self._write_json(code, {"error": f"forced_{code}"})
return
if path == "/health":
self._write_json(200, {"ok": True})
return
if path == "/http/get":
self._write_json(200, {"ok": True, "method": "GET"})
return
if path == "/http/slow":
delay = int(qs.get("seconds", ["2"])[0])
time.sleep(delay)
self._write_json(200, {"ok": True, "delay": delay})
return
if path == "/http/large":
size = int(qs.get("size", ["4096"])[0])
body = ("x" * size).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
return
if path.startswith("/list/"):
out = []
for sha, item in STORE.items():
out.append({
"sha256": sha,
"url": f"http://{HOST}:{PORT}/{sha}",
"size": len(item["bytes"]),
"content_type": item["content_type"],
"created": item["created"],
})
self._write_json(200, out)
return
sha = path.lstrip("/")
item = STORE.get(sha)
if not item:
self._write_json(404, {"error": "not_found"})
return
body = item["bytes"]
self.send_response(200)
self.send_header("Content-Type", item["content_type"])
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_POST(self):
parsed = urlparse(self.path)
path = parsed.path
body = self._read_body()
code, remaining = self._maybe_error_prefix()
if code is not None:
self._write_json(code, {"error": f"forced_{code}"})
return
if path == "/http/post":
self._write_json(200, {
"ok": True,
"method": "POST",
"content_type": self.headers.get("Content-Type", ""),
"body": body.decode("utf-8", errors="replace"),
})
return
self._write_json(404, {"error": "not_found"})
def do_PUT(self):
parsed = urlparse(self.path)
path = parsed.path
body = self._read_body()
code, remaining = self._maybe_error_prefix()
if code is not None:
self._write_json(code, {"error": f"forced_{code}"})
return
if path == "/http/put":
self._write_json(200, {
"ok": True,
"method": "PUT",
"content_type": self.headers.get("Content-Type", ""),
"body": body.decode("utf-8", errors="replace"),
})
return
if path == "/upload":
sha = hashlib.sha256(body).hexdigest()
ctype = self.headers.get("Content-Type", "application/octet-stream")
created = int(time.time())
STORE[sha] = {"bytes": body, "content_type": ctype, "created": created}
self._write_json(200, {
"sha256": sha,
"url": f"http://{HOST}:{PORT}/{sha}",
"size": len(body),
"content_type": ctype,
"created": created,
})
return
self._write_json(404, {"error": "not_found"})
def do_DELETE(self):
parsed = urlparse(self.path)
path = parsed.path
code, remaining = self._maybe_error_prefix()
if code is not None:
self._write_json(code, {"error": f"forced_{code}"})
return
if path == "/http/delete":
self._write_json(200, {"ok": True, "method": "DELETE"})
return
sha = path.lstrip("/")
if sha in STORE:
del STORE[sha]
self._write_json(200, {"deleted": True, "sha256": sha})
else:
self._write_json(404, {"error": "not_found"})
def log_message(self, fmt, *args):
# keep test output clean
pass
if __name__ == "__main__":
server = ThreadingHTTPServer((HOST, PORT), Handler)
print(f"mock_blossom_server listening on http://{HOST}:{PORT}", flush=True)
server.serve_forever()
+105
View File
@@ -0,0 +1,105 @@
/*
* NOSTR Core Library - NIP-03 Live Network Test
*/
#include "nostr_core/nostr_core.h"
#include "nostr_core/nip003.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#define CALENDAR_URL "https://alice.btc.calendar.opentimestamps.org"
#define POLL_INTERVAL_SEC 60
#define MAX_POLL_ATTEMPTS 60 // 1 hour total
void print_progress(int attempt, int complete, const char* ots_b64) {
time_t now = time(NULL);
char time_str[26];
ctime_r(&now, time_str);
time_str[24] = '\0';
printf("[%s] Attempt %d: Status = %s, Proof Size = %zu bytes\n",
time_str, attempt, complete ? "✅ COMPLETE" : "⏳ PENDING",
ots_b64 ? strlen(ots_b64) : 0);
}
int main() {
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to initialize NOSTR library\n");
return 1;
}
printf("🚀 Starting NIP-03 Live Network Test\n");
printf("Using Calendar: %s\n\n", CALENDAR_URL);
// 1. Create a unique "file" (event ID) to stamp
unsigned char private_key[32];
unsigned char public_key[32];
nostr_generate_keypair(private_key, public_key);
char content[128];
snprintf(content, sizeof(content), "Live NIP-03 test at %ld", time(NULL));
cJSON* event = nostr_create_and_sign_event(1, content, NULL, private_key, 0);
const char* event_id = cJSON_GetObjectItem(event, "id")->valuestring;
printf("📄 Created test event: %s\n", event_id);
// 2. Submit to calendar
printf("📤 Submitting to OpenTimestamps calendar...\n");
char* ots_b64 = nostr_nip03_request_timestamp(event_id, CALENDAR_URL, 15);
if (!ots_b64) {
printf("❌ Failed to submit to calendar. Trying with curl to debug...\n");
char cmd[1024];
snprintf(cmd, sizeof(cmd), "curl -s -X POST --data-binary @- %s/digest <<EOF\n$(echo \"%s\" | xxd -r -p)\nEOF\n", CALENDAR_URL, event_id);
printf("Running: %s\n", cmd);
system(cmd);
cJSON_Delete(event);
nostr_cleanup();
return 1;
}
printf("✅ Initial proof received.\n\n");
// 3. Poll for completion
int complete = 0;
int attempts = 0;
while (!complete && attempts < MAX_POLL_ATTEMPTS) {
attempts++;
complete = nostr_nip03_is_proof_complete(ots_b64);
print_progress(attempts, complete, ots_b64);
if (!complete) {
printf(" (Waiting %d seconds for next poll...)\n", POLL_INTERVAL_SEC);
sleep(POLL_INTERVAL_SEC);
// Try to upgrade
char* upgraded = nostr_nip03_upgrade_proof(ots_b64, CALENDAR_URL, 15);
if (upgraded) {
free(ots_b64);
ots_b64 = upgraded;
}
}
}
if (complete) {
printf("\n🎉 SUCCESS! The proof is now complete with a Bitcoin attestation.\n");
// Create the final Nostr event
cJSON* proof_event = nostr_nip03_create_proof_event(event_id, 1, ots_b64, NULL, private_key);
char* json = cJSON_Print(proof_event);
printf("\nFinal NIP-03 Event:\n%s\n", json);
free(json);
cJSON_Delete(proof_event);
} else {
printf("\n❌ Test timed out after %d minutes. The proof is still pending.\n", attempts);
}
free(ots_b64);
cJSON_Delete(event);
nostr_cleanup();
return 0;
}
+66
View File
@@ -0,0 +1,66 @@
/*
* NOSTR Core Library - NIP-03 Test
*/
#include "nostr_core/nostr_core.h"
#include "nostr_core/nip003.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
void test_nip03_create_proof_event() {
printf("🧪 Testing NIP-03 proof event creation...\n");
unsigned char private_key[32];
unsigned char public_key[32];
nostr_generate_keypair(private_key, public_key);
const char* target_id = "e71c6ea722987debdb60f81f9ea4f604b5ac0664120dd64fb9d23abc4ec7c323";
int target_kind = 1;
const char* ots_b64 = "base64encodedotsdata";
const char* relay = "wss://relay.example.com";
cJSON* event = nostr_nip03_create_proof_event(target_id, target_kind, ots_b64, relay, private_key);
assert(event != NULL);
// Verify kind
cJSON* kind = cJSON_GetObjectItem(event, "kind");
assert(kind != NULL && kind->valueint == 1040);
// Verify content
cJSON* content = cJSON_GetObjectItem(event, "content");
assert(content != NULL && strcmp(content->valuestring, ots_b64) == 0);
// Verify tags
cJSON* tags = cJSON_GetObjectItem(event, "tags");
assert(tags != NULL && cJSON_GetArraySize(tags) == 2);
cJSON* e_tag = cJSON_GetArrayItem(tags, 0);
assert(strcmp(cJSON_GetArrayItem(e_tag, 0)->valuestring, "e") == 0);
assert(strcmp(cJSON_GetArrayItem(e_tag, 1)->valuestring, target_id) == 0);
assert(strcmp(cJSON_GetArrayItem(e_tag, 2)->valuestring, relay) == 0);
cJSON* k_tag = cJSON_GetArrayItem(tags, 1);
assert(strcmp(cJSON_GetArrayItem(k_tag, 0)->valuestring, "k") == 0);
assert(strcmp(cJSON_GetArrayItem(k_tag, 1)->valuestring, "1") == 0);
printf("✅ NIP-03 proof event creation test passed!\n");
cJSON_Delete(event);
}
int main() {
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to initialize NOSTR library\n");
return 1;
}
test_nip03_create_proof_event();
// Note: We don't test nostr_nip03_request_timestamp here as it requires a live OTS calendar
// and network access, which might be flaky in a test environment.
nostr_cleanup();
printf("\n🎉 All NIP-03 tests passed!\n");
return 0;
}
File diff suppressed because it is too large Load Diff
+150
View File
@@ -0,0 +1,150 @@
/*
* nostr_http unit tests using local mock server.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../nostr_core/nostr_http.h"
#include "../nostr_core/nostr_common.h"
static int tests_run = 0;
static int tests_passed = 0;
#define TEST_ASSERT(cond, msg) do { \
tests_run++; \
if (cond) { tests_passed++; printf("✅ %s\n", msg); } \
else { printf("❌ %s\n", msg); } \
} while (0)
static const char* base_url(void) {
const char* env = getenv("MOCK_BLOSSOM_BASE");
return (env && env[0] != '\0') ? env : "http://127.0.0.1:18081";
}
static void make_url(char* out, size_t out_size, const char* path) {
snprintf(out, out_size, "%s%s", base_url(), path ? path : "");
}
static void test_get_and_post_helpers(void) {
char url[512];
make_url(url, sizeof(url), "/http/get");
char* body = NULL;
long status = 0;
int rc = nostr_http_get(url, 5, &body, &status);
TEST_ASSERT(rc == NOSTR_SUCCESS, "nostr_http_get transport success");
TEST_ASSERT(status == 200, "nostr_http_get status=200");
TEST_ASSERT(body && strstr(body, "\"method\": \"GET\""), "nostr_http_get response body contains method");
free(body);
make_url(url, sizeof(url), "/http/post");
body = NULL;
status = 0;
rc = nostr_http_post_json(url, "{\"hello\":\"world\"}", 5, &body, &status);
TEST_ASSERT(rc == NOSTR_SUCCESS, "nostr_http_post_json transport success");
TEST_ASSERT(status == 200, "nostr_http_post_json status=200");
TEST_ASSERT(body && strstr(body, "\"method\": \"POST\""), "nostr_http_post_json response contains method");
free(body);
}
static void test_request_methods_and_headers(void) {
char url[512];
int rc;
nostr_http_request_t req;
nostr_http_response_t resp;
make_url(url, sizeof(url), "/http/put");
memset(&req, 0, sizeof(req));
req.method = "PUT";
req.url = url;
req.body = (const unsigned char*)"abc";
req.body_len = 3;
req.timeout_seconds = 5;
rc = nostr_http_request(&req, &resp);
TEST_ASSERT(rc == NOSTR_SUCCESS, "nostr_http_request PUT transport success");
TEST_ASSERT(resp.status_code == 200, "nostr_http_request PUT status=200");
TEST_ASSERT(resp.body && strstr(resp.body, "\"method\": \"PUT\""), "nostr_http_request PUT response contains method");
nostr_http_response_free(&resp);
make_url(url, sizeof(url), "/http/delete");
memset(&req, 0, sizeof(req));
req.method = "DELETE";
req.url = url;
req.timeout_seconds = 5;
rc = nostr_http_request(&req, &resp);
TEST_ASSERT(rc == NOSTR_SUCCESS, "nostr_http_request DELETE transport success");
TEST_ASSERT(resp.status_code == 200, "nostr_http_request DELETE status=200");
TEST_ASSERT(resp.body && strstr(resp.body, "\"method\": \"DELETE\""), "nostr_http_request DELETE response contains method");
nostr_http_response_free(&resp);
make_url(url, sizeof(url), "/http/head");
memset(&req, 0, sizeof(req));
req.method = "HEAD";
req.url = url;
req.timeout_seconds = 5;
req.capture_headers = 1;
rc = nostr_http_request(&req, &resp);
TEST_ASSERT(rc == NOSTR_SUCCESS, "nostr_http_request HEAD transport success");
TEST_ASSERT(resp.status_code == 200, "nostr_http_request HEAD status=200");
TEST_ASSERT(resp.headers_raw != NULL, "nostr_http_request HEAD captured headers");
nostr_http_response_free(&resp);
}
static void test_timeout_and_max_response_bytes(void) {
char url[512];
int rc;
nostr_http_request_t req;
nostr_http_response_t resp;
make_url(url, sizeof(url), "/http/slow?seconds=2");
memset(&req, 0, sizeof(req));
req.method = "GET";
req.url = url;
req.timeout_seconds = 1;
rc = nostr_http_request(&req, &resp);
TEST_ASSERT(rc == NOSTR_ERROR_NETWORK_FAILED, "nostr_http_request timeout returns network error");
make_url(url, sizeof(url), "/http/large?size=4096");
memset(&req, 0, sizeof(req));
req.method = "GET";
req.url = url;
req.timeout_seconds = 5;
req.max_response_bytes = 128;
rc = nostr_http_request(&req, &resp);
TEST_ASSERT(rc == NOSTR_SUCCESS, "nostr_http_request large body transport success");
TEST_ASSERT(resp.status_code == 200, "nostr_http_request large body status=200");
TEST_ASSERT(resp.body_len <= 128, "nostr_http_request respects max_response_bytes");
TEST_ASSERT(resp.truncated == 1, "nostr_http_request marks response as truncated");
nostr_http_response_free(&resp);
}
static void test_ca_bundle_helpers(void) {
const char* detected = nostr_http_detect_ca_bundle();
TEST_ASSERT(detected == NULL || detected[0] != '\0', "nostr_http_detect_ca_bundle callable");
nostr_http_set_ca_bundle("");
TEST_ASSERT(1, "nostr_http_set_ca_bundle accepts empty string");
if (detected && detected[0] != '\0') {
nostr_http_set_ca_bundle(detected);
TEST_ASSERT(1, "nostr_http_set_ca_bundle accepts detected CA path");
}
}
int main(void) {
printf("nostr_http Unit Tests\n");
printf("=====================\n");
printf("Base URL: %s\n", base_url());
test_get_and_post_helpers();
test_request_methods_and_headers();
test_timeout_and_max_response_bytes();
test_ca_bundle_helpers();
printf("\nSummary: %d/%d passed\n", tests_passed, tests_run);
return (tests_passed == tests_run) ? 0 : 1;
}