Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84257b2f58 | ||
|
|
af4f7cace9 | ||
|
|
363b7e9adf | ||
|
|
3186c2ead6 | ||
|
|
f07433578a | ||
|
|
d264d9f5ed | ||
|
|
98094cac86 | ||
|
|
79d98587d3 | ||
|
|
81814e8518 | ||
|
|
784cfc09c3 | ||
|
|
c87f8db6d8 | ||
|
|
c871c40219 | ||
|
|
2b3113dafc | ||
|
|
5c51ab1f9f | ||
|
|
df50ca1273 | ||
|
|
2ce2ecbbfb | ||
|
|
57ecbf0c11 | ||
|
|
e87d7364ff | ||
|
|
0b2cca6549 | ||
|
|
fca1a8077d | ||
|
|
2c128a6295 | ||
|
|
24903b8c90 | ||
|
|
c6896eebb4 | ||
|
|
4e06d8f723 | ||
|
|
d7a8aa1dc4 | ||
|
|
cdeb569a16 | ||
|
|
2224448d12 | ||
|
|
9bf74e76ce | ||
|
|
d52ac9c106 | ||
|
|
e087a17eb9 | ||
|
|
d7e43328bb | ||
|
|
0af77dffab | ||
|
|
0f72d7433a | ||
|
|
2f3140d4f6 | ||
|
|
31947d12b7 | ||
|
|
9e9fcba96c | ||
|
|
4a096ff35f | ||
|
|
64aee10a21 | ||
|
|
a9a1aff682 | ||
|
|
a4fbba027e | ||
|
|
fdc7cc2a38 | ||
|
|
9879457d7e | ||
|
|
6722d09579 | ||
|
|
3533c874ea | ||
|
|
553774fdc4 | ||
|
|
3f5024c24c | ||
|
|
8bcf76387b | ||
|
|
7bf39a7e10 | ||
|
|
ec5a97f951 | ||
|
|
9913f92c96 | ||
|
|
dad1f5aa52 | ||
|
|
241ca036ed | ||
|
|
997c3e976b | ||
|
|
ea9b8918f6 | ||
|
|
52e5d300fc | ||
|
|
ace0b5a792 | ||
|
|
04f22e6f47 | ||
|
|
e8c0fcdf63 | ||
|
|
7cf7ffe025 | ||
|
|
a2b700cf40 | ||
|
|
dfe8a20e2e | ||
|
|
3cfddbe75b | ||
|
|
8c610b6e36 | ||
|
|
2de890da5c | ||
|
|
bb673cc1a4 | ||
|
|
f6c8ef6246 | ||
|
|
01d382c0d3 | ||
|
|
f86fb256b2 | ||
|
|
ac85ab6f07 | ||
|
|
cfde3bcb0f | ||
|
|
a4ae8df66f | ||
|
|
3108661b0a | ||
|
|
3b076ea372 | ||
|
|
6369fb0cf3 |
-21
@@ -19,24 +19,3 @@ node_modules/
|
||||
*.dylib
|
||||
*.dll
|
||||
build/
|
||||
|
||||
# Build outputs/binaries (do not track)
|
||||
/websocket_debug
|
||||
/libnostr_core_arm64.a
|
||||
/tests/websocket_debug
|
||||
/tests/*_test
|
||||
/examples/input_detection
|
||||
/examples/keypair_generation
|
||||
/examples/mnemonic_derivation
|
||||
/examples/mnemonic_generation
|
||||
/examples/ots_tool
|
||||
/examples/relay_pool
|
||||
/examples/send_nip17_dm
|
||||
/examples/simple_keygen
|
||||
/examples/timestamping
|
||||
/examples/utility_functions
|
||||
/examples/version_test
|
||||
|
||||
# Local OpenTimestamps CLI output
|
||||
/examples/timestamped_*
|
||||
/examples/*.ots
|
||||
|
||||
-119
@@ -1,119 +0,0 @@
|
||||
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/nostr_signer.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/nip003.c
|
||||
nostr_core/nip004.c
|
||||
nostr_core/nip005.c
|
||||
nostr_core/nip006.c
|
||||
nostr_core/nip011.c
|
||||
nostr_core/nip013.c
|
||||
nostr_core/nip017.c
|
||||
nostr_core/nip019.c
|
||||
nostr_core/nip021.c
|
||||
nostr_core/nip042.c
|
||||
nostr_core/nip044.c
|
||||
nostr_core/nip046.c
|
||||
nostr_core/nip059.c
|
||||
nostr_core/nip060.c
|
||||
nostr_core/nip061.c
|
||||
nostr_core/nostr_signer.c
|
||||
nostr_core/nsigner_transport.c
|
||||
nostr_core/nsigner_client.c
|
||||
nostr_core/utils.c
|
||||
nostr_core/nostr_http.c
|
||||
nostr_core/core_relays.c
|
||||
nostr_core/core_relay_pool.c
|
||||
nostr_core/cashu_mint.c
|
||||
nostr_core/blossom_client.c
|
||||
nostr_core/crypto/nostr_secp256k1.c
|
||||
nostr_core/crypto/nostr_aes.c
|
||||
nostr_core/crypto/nostr_chacha20.c
|
||||
nostr_core/crypto/nostr_poly1305.c
|
||||
nostr_core/crypto/nostr_chacha20poly1305.c
|
||||
cjson/cJSON.c
|
||||
nostr_websocket/nostr_websocket_openssl.c
|
||||
platform/linux.c
|
||||
)
|
||||
|
||||
target_include_directories(nostr_core PUBLIC
|
||||
.
|
||||
nostr_core
|
||||
nostr_core/crypto
|
||||
cjson
|
||||
nostr_websocket
|
||||
)
|
||||
target_compile_definitions(nostr_core PRIVATE NOSTR_ENABLE_NSIGNER_CLIENT=1)
|
||||
target_compile_definitions(nostr_core PRIVATE ENABLE_FILE_LOGGING ENABLE_WEBSOCKET_LOGGING ENABLE_DEBUG_LOGGING)
|
||||
|
||||
# Link system dependencies
|
||||
find_package(PkgConfig QUIET)
|
||||
if(PkgConfig_FOUND)
|
||||
pkg_check_modules(SECP256K1 QUIET libsecp256k1)
|
||||
pkg_check_modules(OPENSSL QUIET openssl)
|
||||
pkg_check_modules(CURL QUIET libcurl)
|
||||
endif()
|
||||
|
||||
if(SECP256K1_FOUND)
|
||||
target_include_directories(nostr_core PRIVATE ${SECP256K1_INCLUDE_DIRS})
|
||||
target_link_libraries(nostr_core PRIVATE ${SECP256K1_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(nostr_core PRIVATE secp256k1)
|
||||
endif()
|
||||
|
||||
if(OPENSSL_FOUND)
|
||||
target_include_directories(nostr_core PRIVATE ${OPENSSL_INCLUDE_DIRS})
|
||||
target_link_libraries(nostr_core PRIVATE ${OPENSSL_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(nostr_core PRIVATE ssl crypto)
|
||||
endif()
|
||||
|
||||
if(CURL_FOUND)
|
||||
target_include_directories(nostr_core PRIVATE ${CURL_INCLUDE_DIRS})
|
||||
target_link_libraries(nostr_core PRIVATE ${CURL_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(nostr_core PRIVATE curl)
|
||||
endif()
|
||||
|
||||
target_link_libraries(nostr_core PRIVATE z dl pthread m)
|
||||
endif()
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
A C library for NOSTR protocol implementation. Work in progress.
|
||||
|
||||
[](VERSION)
|
||||
[](VERSION)
|
||||
[](#license)
|
||||
[](#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
|
||||
- [x] [NIP-03](nips/03.md) - OpenTimestamps Attestations for Events
|
||||
- [ ] [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 | ➖ Not Applicable
|
||||
**Legend:** ✅ Fully Implemented | ⚠️ Partial Implementation | ❌ Not Implemented
|
||||
|
||||
**Implementation Summary:** 13 of 96+ NIPs fully implemented (13.5%)
|
||||
|
||||
@@ -346,171 +346,6 @@ int nostr_nip11_fetch_relay_info(const char* relay_url, nostr_relay_info_t** inf
|
||||
void nostr_nip11_relay_info_free(nostr_relay_info_t* info);
|
||||
```
|
||||
|
||||
## Signer abstraction & nsigner integration
|
||||
|
||||
All signing/encryption operations can run through an opaque `nostr_signer_t` handle. This provides:
|
||||
|
||||
- **LOCAL backend** (`nostr_signer_local`) using a raw 32-byte private key (output-equivalent to legacy raw-key APIs).
|
||||
- **REMOTE backends** (`nostr_signer_nsigner_*`) that call a running `n_signer` process/device over supported transports.
|
||||
|
||||
### Core signer verbs (exact signatures)
|
||||
|
||||
```c
|
||||
/* Lifecycle */
|
||||
nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]);
|
||||
void nostr_signer_free(nostr_signer_t* signer);
|
||||
|
||||
/* Core verbs */
|
||||
int nostr_signer_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]);
|
||||
int nostr_signer_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out);
|
||||
|
||||
/* Encryption verbs */
|
||||
int nostr_signer_nip04_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out);
|
||||
int nostr_signer_nip04_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out);
|
||||
int nostr_signer_nip44_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out);
|
||||
int nostr_signer_nip44_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out);
|
||||
```
|
||||
|
||||
### Backends / transports
|
||||
|
||||
Remote signer factories (available when `NOSTR_ENABLE_NSIGNER_CLIENT` is enabled):
|
||||
|
||||
```c
|
||||
nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_serial(const char* device_path, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_tcp(const char* host, int port, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_fds(int read_fd, int write_fd, const char* role, int timeout_ms);
|
||||
int nostr_signer_nsigner_set_auth(nostr_signer_t* signer,
|
||||
const unsigned char auth_privkey[32],
|
||||
const char* label);
|
||||
```
|
||||
|
||||
Transport layer constructors and discovery helpers:
|
||||
|
||||
```c
|
||||
nsigner_transport_t* nsigner_transport_open_unix(const char* socket_name, int timeout_ms);
|
||||
nsigner_transport_t* nsigner_transport_open_tcp(const char* host, int port, int timeout_ms);
|
||||
nsigner_transport_t* nsigner_transport_open_serial(const char* device_path, int timeout_ms);
|
||||
nsigner_transport_t* nsigner_transport_open_fds(int read_fd, int write_fd, int timeout_ms);
|
||||
int nsigner_transport_list_unix(char names[][64], int max_names);
|
||||
int nsigner_transport_list_serial(char paths[][64], int max_paths);
|
||||
```
|
||||
|
||||
Supported transport modes:
|
||||
- UNIX abstract socket (`unix:<name>`, no leading `@` in API input)
|
||||
- USB CDC-ACM serial (`serial:/dev/ttyACM0`)
|
||||
- TCP (`tcp:<host>:<port>`)
|
||||
- Existing framed fd pair (`fds:<read_fd>:<write_fd>`, useful for stdio/qrexec-style bridges)
|
||||
|
||||
Build gating:
|
||||
- Desktop builds enable `NOSTR_ENABLE_NSIGNER_CLIENT`.
|
||||
- ESP32 builds exclude the nsigner client transport/client sources and do not define the flag.
|
||||
- n_signer’s own build keeps this client side disabled.
|
||||
|
||||
Wire contract (high level):
|
||||
- Framing is **4-byte big-endian length + JSON payload**.
|
||||
- Payload length must be in `[1, 65536]` bytes.
|
||||
- TCP uses a kind-27235 auth envelope (`nsigner_rpc`, `nsigner_method`, `nsigner_body_hash`).
|
||||
- See n_signer docs for authoritative protocol details.
|
||||
|
||||
### Using a signer in higher-level APIs
|
||||
|
||||
Pattern: existing raw-private-key functions remain unchanged; signer-aware siblings are provided as `*_with_signer` variants.
|
||||
|
||||
Covered modules and functions:
|
||||
|
||||
- NIP-01 (`nostr_core/nip001.h`)
|
||||
- `cJSON* nostr_create_and_sign_event_with_signer(int kind, const char* content, cJSON* tags, nostr_signer_t* signer, time_t timestamp);`
|
||||
|
||||
- NIP-03 OpenTimestamps (`nostr_core/nip003.h`)
|
||||
- `cJSON* nostr_nip03_create_proof_event_with_signer(const char* target_event_id, int target_event_kind, const char* ots_data_base64, const char* relay_url, nostr_signer_t* signer);`
|
||||
- `char* nostr_nip03_request_timestamp(const char* event_id_hex, const char* calendar_url, int timeout_seconds);`
|
||||
- `int nostr_nip03_is_proof_complete(const char* ots_data_base64);`
|
||||
- `int nostr_nip03_get_attestation_info(const char* ots_data_base64, int* has_bitcoin, int* has_litecoin, int* has_pending, uint64_t* bitcoin_height, uint64_t* litecoin_height, char** pending_uri_out);`
|
||||
- `char* nostr_nip03_upgrade_proof(const char* ots_data_base64, const char* calendar_url, int timeout_seconds);`
|
||||
- `int nostr_nip03_verify_proof(const char* ots_data_base64, const char* target_digest_hex, uint64_t* block_height_out, time_t* block_time_out);`
|
||||
|
||||
- NIP-17 (`nostr_core/nip017.h`)
|
||||
- `cJSON* nostr_nip17_create_relay_list_event_with_signer(const char** relay_urls, int num_relays, nostr_signer_t* signer);`
|
||||
- `int nostr_nip17_send_dm_with_signer(cJSON* dm_event, const char** recipient_pubkeys, int num_recipients, nostr_signer_t* signer, cJSON** gift_wraps_out, int max_gift_wraps, long max_delay_sec);`
|
||||
- `cJSON* nostr_nip17_receive_dm_with_signer(cJSON* gift_wrap, nostr_signer_t* signer);`
|
||||
|
||||
- NIP-42 (`nostr_core/nip042.h`)
|
||||
- `cJSON* nostr_nip42_create_auth_event_with_signer(const char* challenge, const char* relay_url, nostr_signer_t* signer, time_t timestamp);`
|
||||
|
||||
- NIP-46 client/event helpers (`nostr_core/nip046.h`)
|
||||
- `cJSON* nostr_nip46_create_request_event_with_signer(const nostr_nip46_request_t* request, nostr_signer_t* signer, const unsigned char* recipient_public_key, time_t timestamp);`
|
||||
- `cJSON* nostr_nip46_create_response_event_with_signer(const nostr_nip46_response_t* response, nostr_signer_t* signer, const unsigned char* recipient_public_key, time_t timestamp);`
|
||||
- `int nostr_nip46_decrypt_event_with_signer(cJSON* event, nostr_signer_t* signer, char** output_out);`
|
||||
- `int nostr_nip46_client_session_init_with_signer(nostr_nip46_client_session_t* session, nostr_signer_t* signer, const char* bunker_url);`
|
||||
|
||||
- NIP-60 (`nostr_core/nip060.h`)
|
||||
- `cJSON* nostr_nip60_create_wallet_event_with_signer(const nostr_nip60_wallet_data_t* wallet_data, nostr_signer_t* signer, time_t timestamp);`
|
||||
- `cJSON* nostr_nip60_create_token_event_with_signer(const nostr_nip60_token_data_t* token_data, nostr_signer_t* signer, time_t timestamp);`
|
||||
- `cJSON* nostr_nip60_create_token_deletion_with_signer(const char* token_event_id, nostr_signer_t* signer, time_t timestamp);`
|
||||
- `cJSON* nostr_nip60_create_rollover_token_with_signer(const nostr_nip60_token_data_t* remaining_proofs, const char** deleted_event_ids, int deleted_count, nostr_signer_t* signer, time_t timestamp);`
|
||||
- `cJSON* nostr_nip60_create_history_event_with_signer(const nostr_nip60_history_data_t* history_data, nostr_signer_t* signer, time_t timestamp);`
|
||||
- `cJSON* nostr_nip60_create_quote_event_with_signer(const char* quote_id, const char* mint_url, time_t expiration, nostr_signer_t* signer, time_t timestamp);`
|
||||
|
||||
- NIP-61 (`nostr_core/nip061.h`)
|
||||
- `cJSON* nostr_nip61_create_nutzap_info_event_with_signer(const nostr_nip61_nutzap_info_t* info, nostr_signer_t* signer, time_t timestamp);`
|
||||
- `cJSON* nostr_nip61_create_nutzap_event_with_signer(const nostr_nip61_nutzap_data_t* nutzap_data, nostr_signer_t* signer, time_t timestamp);`
|
||||
- `cJSON* nostr_nip61_create_redemption_event_with_signer(const char* nutzap_event_id, const char* nutzap_relay_hint, const char* sender_pubkey, const char* created_token_event_id, const char* created_token_relay_hint, uint64_t amount, nostr_signer_t* signer, time_t timestamp);`
|
||||
|
||||
- Blossom auth/upload/delete (`nostr_core/blossom_client.h`)
|
||||
- `char* blossom_create_auth_header_with_signer(nostr_signer_t* signer, const char* operation, const char* sha256_hex, int expiration_seconds, time_t timestamp);`
|
||||
- `int blossom_upload_with_signer(const char* server_url, const unsigned char* data, size_t data_len, const char* content_type, nostr_signer_t* signer, const char* sha256_hex, int timeout_seconds, blossom_blob_descriptor_t* descriptor_out);`
|
||||
- `int blossom_upload_file_with_signer(const char* server_url, const char* file_path, const char* content_type, nostr_signer_t* signer, int timeout_seconds, blossom_blob_descriptor_t* descriptor_out);`
|
||||
- `int blossom_delete_with_signer(const char* server_url, const char* sha256_hex, nostr_signer_t* signer, int timeout_seconds);`
|
||||
|
||||
- Relay-pool NIP-42 AUTH entry point (`nostr_core/nostr_core.h`)
|
||||
- `int nostr_relay_pool_set_auth_with_signer(nostr_relay_pool_t* pool, nostr_signer_t* signer, int enable);`
|
||||
|
||||
### Driver app: `examples/note_poster`
|
||||
|
||||
Signer modes:
|
||||
|
||||
```bash
|
||||
./examples/note_poster
|
||||
```
|
||||
|
||||
Default is local signer mode; this prompts for `nsec1...` or 64-char hex private key.
|
||||
|
||||
**WARNING:** test-key-only flow. Do not use a production key; input key material is held in process memory in plaintext.
|
||||
|
||||
```bash
|
||||
./examples/note_poster --signer unix:<name>
|
||||
./examples/note_poster --signer serial:/dev/ttyACM0
|
||||
./examples/note_poster --signer tcp:<host>:<port>
|
||||
./examples/note_poster --signer fds:<read_fd>:<write_fd>
|
||||
```
|
||||
|
||||
### Boundaries / what stays in n_signer
|
||||
|
||||
`nostr_core_lib` provides the **client side only** (transports + nsigner client + signer backends). The following stay in the `n_signer` project:
|
||||
|
||||
- process spawning/embedding and deployment packaging
|
||||
- signer program trust-boundary features (mnemonic handling, approvals, role policy)
|
||||
- hardware firmware and platform/device-specific runtime pieces
|
||||
|
||||
Dependency direction remains one-way: **`n_signer -> nostr_core_lib`**.
|
||||
|
||||
Operations requiring raw secret material outside signer verbs are not remotable as-is. Example: mint-protocol operations in Cashu mint pieces (`cashu_mint`) that require direct secret access beyond event signing/NIP-04/NIP-44.
|
||||
|
||||
For implementation plan context see `plans/nsigner_integration_plan.md`. This integration supersedes per-project hand-rolled signer clients (future migration milestone M7).
|
||||
|
||||
## 📁 Examples
|
||||
|
||||
The library includes comprehensive examples:
|
||||
@@ -673,20 +508,6 @@ 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
|
||||
@@ -706,22 +527,20 @@ Reference ESP-IDF example project (in this workspace):
|
||||
|
||||
## 📈 Version History
|
||||
|
||||
Current version: **0.6.0**
|
||||
Current version: **0.4.8**
|
||||
|
||||
The library uses automatic semantic versioning based on Git tags. Each build increments the patch version automatically.
|
||||
|
||||
**Recent Developments:**
|
||||
- **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
|
||||
- **OpenSSL Migration**: Transitioned from mbedTLS to OpenSSL for improved compatibility
|
||||
- **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.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.4.x` - Current 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
|
||||
@@ -761,7 +580,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 -t` to verify your environment
|
||||
- Run `./build.sh test` to verify your environment
|
||||
- Review the comprehensive API documentation in `nostr_core/nostr_core.h`
|
||||
|
||||
## 📜 License
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
0.6.6
|
||||
Binary file not shown.
@@ -135,7 +135,6 @@ 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"
|
||||
@@ -198,7 +197,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 003 004 005 006 011 013 017 019 021 042 044 046 059 060 061"
|
||||
NEEDED_NIPS="001 004 005 006 011 013 017 019 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
|
||||
@@ -217,7 +216,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 003 004 005 006 011 013 019 021 042 044 046 059 060 061"
|
||||
NEEDED_NIPS="001 004 005 006 011 013 019 042 044 046 059 060 061"
|
||||
elif [ -n "$DETECTED" ]; then
|
||||
NEEDED_NIPS="$DETECTED"
|
||||
print_success "Auto-detected NIPs: $(echo $NEEDED_NIPS | tr ' ' ',')"
|
||||
@@ -235,7 +234,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 003 004 005 006 011 013 017 019 021 042 044 046 059 060 061"
|
||||
NEEDED_NIPS="001 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
|
||||
|
||||
@@ -500,21 +499,14 @@ 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"
|
||||
SOURCES="$SOURCES nostr_core/nostr_signer.c"
|
||||
SOURCES="$SOURCES nostr_core/core_relays.c"
|
||||
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"
|
||||
SOURCES="$SOURCES nostr_core/nsigner_transport.c"
|
||||
SOURCES="$SOURCES nostr_core/nsigner_client.c"
|
||||
|
||||
NIP_DESCRIPTIONS=""
|
||||
|
||||
@@ -524,7 +516,6 @@ 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)" ;;
|
||||
@@ -549,12 +540,9 @@ 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"
|
||||
CFLAGS="$CFLAGS -DNOSTR_ENABLE_NSIGNER_CLIENT=1"
|
||||
INCLUDES="-I. -Inostr_core -Inostr_core/crypto -Icjson -Inostr_websocket"
|
||||
|
||||
# Add system library includes
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,300 +0,0 @@
|
||||
/*
|
||||
* note_poster - minimal sign-in + kind-1 post test app
|
||||
*
|
||||
* Supports signer modes:
|
||||
* - local (default): sign in with a local key (nsec or hex private key)
|
||||
* - unix:<name>: use remote nsigner over AF_UNIX abstract socket
|
||||
* - serial:<path>: use remote nsigner over USB CDC serial (e.g. /dev/ttyACM0)
|
||||
* - tcp:<host>:<port>: use remote nsigner over TCP (auth envelope required by n_signer)
|
||||
* - fds:<read_fd>:<write_fd>: use remote nsigner over existing framed fd pair (stdio/qrexec helper)
|
||||
*
|
||||
* Creates and signs a kind-1 note through nostr_signer_t,
|
||||
* then publishes to wss://relay.laantungir.net
|
||||
*/
|
||||
|
||||
#include "../nostr_core/nostr_core.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#define DEFAULT_RELAY "wss://relay.laantungir.net"
|
||||
|
||||
static void publish_progress_callback(const char* relay_url,
|
||||
const char* status,
|
||||
const char* message,
|
||||
int success_count,
|
||||
int total_relays,
|
||||
int completed_relays,
|
||||
void* user_data) {
|
||||
(void)user_data;
|
||||
|
||||
if (relay_url) {
|
||||
printf("[%s] %s", relay_url, status ? status : "(unknown)");
|
||||
if (message) {
|
||||
printf(" - %s", message);
|
||||
}
|
||||
printf(" (%d/%d complete, %d success)\n", completed_relays, total_relays, success_count);
|
||||
}
|
||||
}
|
||||
|
||||
static int read_line(const char* prompt, char* out, size_t out_sz) {
|
||||
size_t len;
|
||||
if (!prompt || !out || out_sz == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
printf("%s", prompt);
|
||||
if (!fgets(out, (int)out_sz, stdin)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = strlen(out);
|
||||
if (len > 0 && out[len - 1] == '\n') {
|
||||
out[len - 1] = '\0';
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_private_key_input(const char* input, unsigned char out_privkey[32]) {
|
||||
if (!input || !out_privkey) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strncmp(input, "nsec1", 5) == 0) {
|
||||
return nostr_decode_nsec(input, out_privkey);
|
||||
}
|
||||
|
||||
if (strlen(input) == 64) {
|
||||
return nostr_hex_to_bytes(input, out_privkey, 32);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
unsigned char private_key[32] = {0};
|
||||
char key_input[256] = {0};
|
||||
char note_content[2048] = {0};
|
||||
nostr_signer_t* signer = NULL;
|
||||
char pubkey_hex[65] = {0};
|
||||
cJSON* note_event = NULL;
|
||||
cJSON* id_item;
|
||||
const char* relay_urls[] = {DEFAULT_RELAY};
|
||||
int success_count = 0;
|
||||
publish_result_t* results = NULL;
|
||||
enum {
|
||||
SIGNER_MODE_LOCAL = 0,
|
||||
SIGNER_MODE_UNIX = 1,
|
||||
SIGNER_MODE_SERIAL = 2,
|
||||
SIGNER_MODE_TCP = 3,
|
||||
SIGNER_MODE_FDS = 4
|
||||
} signer_mode = SIGNER_MODE_LOCAL;
|
||||
const char* unix_name = NULL;
|
||||
const char* serial_path = NULL;
|
||||
char tcp_host[256] = {0};
|
||||
int tcp_port = 0;
|
||||
int fds_read_fd = -1;
|
||||
int fds_write_fd = -1;
|
||||
int argi = 1;
|
||||
|
||||
if (argc >= 3 && strcmp(argv[1], "--signer") == 0) {
|
||||
if (strncmp(argv[2], "unix:", 5) == 0 && argv[2][5] != '\0') {
|
||||
signer_mode = SIGNER_MODE_UNIX;
|
||||
unix_name = argv[2] + 5;
|
||||
} else if (strncmp(argv[2], "serial:", 7) == 0 && argv[2][7] != '\0') {
|
||||
signer_mode = SIGNER_MODE_SERIAL;
|
||||
serial_path = argv[2] + 7;
|
||||
} else if (strncmp(argv[2], "tcp:", 4) == 0 && argv[2][4] != '\0') {
|
||||
const char* spec = argv[2] + 4;
|
||||
const char* sep = strrchr(spec, ':');
|
||||
char* endptr = NULL;
|
||||
long parsed_port;
|
||||
size_t host_len;
|
||||
|
||||
if (sep == NULL || sep == spec || sep[1] == '\0') {
|
||||
fprintf(stderr, "Invalid tcp signer format. Use tcp:<host>:<port>\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
host_len = (size_t)(sep - spec);
|
||||
if (host_len >= sizeof(tcp_host)) {
|
||||
fprintf(stderr, "TCP host too long\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
memcpy(tcp_host, spec, host_len);
|
||||
tcp_host[host_len] = '\0';
|
||||
|
||||
parsed_port = strtol(sep + 1, &endptr, 10);
|
||||
if (endptr == NULL || *endptr != '\0' || parsed_port <= 0 || parsed_port > 65535) {
|
||||
fprintf(stderr, "Invalid TCP port in --signer tcp:<host>:<port>\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
signer_mode = SIGNER_MODE_TCP;
|
||||
tcp_port = (int)parsed_port;
|
||||
} else if (strncmp(argv[2], "fds:", 4) == 0 && argv[2][4] != '\0') {
|
||||
int parsed = -1;
|
||||
parsed = sscanf(argv[2] + 4, "%d:%d", &fds_read_fd, &fds_write_fd);
|
||||
if (parsed != 2 || fds_read_fd < 0 || fds_write_fd < 0) {
|
||||
fprintf(stderr, "Invalid fds signer format. Use fds:<read_fd>:<write_fd>\n");
|
||||
return 1;
|
||||
}
|
||||
signer_mode = SIGNER_MODE_FDS;
|
||||
} else if (strcmp(argv[2], "local") == 0) {
|
||||
signer_mode = SIGNER_MODE_LOCAL;
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
"Invalid --signer value. Use --signer local, --signer unix:<name>, --signer serial:<path>, --signer tcp:<host>:<port>, or --signer fds:<read_fd>:<write_fd>\n");
|
||||
return 1;
|
||||
}
|
||||
argi = 3;
|
||||
}
|
||||
|
||||
if (signer_mode == SIGNER_MODE_LOCAL) {
|
||||
if (argc > argi) {
|
||||
strncpy(key_input, argv[argi], sizeof(key_input) - 1);
|
||||
argi++;
|
||||
} else if (read_line("Enter nsec or 64-char hex private key: ", key_input, sizeof(key_input)) != 0) {
|
||||
fprintf(stderr, "Failed to read key input\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "WARNING: This is a TEST-KEY-ONLY tool. Do not use a real/production nsec. Keys are read into process memory in plaintext.\n");
|
||||
if (parse_private_key_input(key_input, private_key) != 0) {
|
||||
fprintf(stderr, "Invalid private key input (must be nsec1... or 64-char hex)\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (argc > argi) {
|
||||
strncpy(note_content, argv[argi], sizeof(note_content) - 1);
|
||||
} else if (read_line("Enter note content: ", note_content, sizeof(note_content)) != 0) {
|
||||
fprintf(stderr, "Failed to read note content\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to initialize nostr library\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (signer_mode == SIGNER_MODE_UNIX) {
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
signer = nostr_signer_nsigner_unix(unix_name, NULL, 15000);
|
||||
if (!signer) {
|
||||
fprintf(stderr, "Failed to initialize unix nsigner backend (%s)\n", unix_name);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
#else
|
||||
fprintf(stderr, "This build does not include nsigner client support\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
#endif
|
||||
} else if (signer_mode == SIGNER_MODE_SERIAL) {
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
signer = nostr_signer_nsigner_serial(serial_path, NULL, 15000);
|
||||
if (!signer) {
|
||||
fprintf(stderr, "Failed to initialize serial nsigner backend (%s)\n", serial_path);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
#else
|
||||
fprintf(stderr, "This build does not include nsigner client support\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
#endif
|
||||
} else if (signer_mode == SIGNER_MODE_TCP) {
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
signer = nostr_signer_nsigner_tcp(tcp_host, tcp_port, NULL, 15000);
|
||||
if (!signer) {
|
||||
fprintf(stderr, "Failed to initialize tcp nsigner backend (%s:%d)\n", tcp_host, tcp_port);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
#else
|
||||
fprintf(stderr, "This build does not include nsigner client support\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
#endif
|
||||
} else if (signer_mode == SIGNER_MODE_FDS) {
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
signer = nostr_signer_nsigner_fds(fds_read_fd, fds_write_fd, NULL, 15000);
|
||||
if (!signer) {
|
||||
fprintf(stderr, "Failed to initialize fds nsigner backend (%d:%d)\n", fds_read_fd, fds_write_fd);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
#else
|
||||
fprintf(stderr, "This build does not include nsigner client support\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
#endif
|
||||
} else {
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
fprintf(stderr, "Failed to initialize local signer\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (nostr_signer_get_public_key(signer, pubkey_hex) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to get signer pubkey\n");
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Signed in as pubkey: %s\n", pubkey_hex);
|
||||
|
||||
note_event = nostr_create_and_sign_event_with_signer(1, note_content, NULL, signer, time(NULL));
|
||||
if (!note_event) {
|
||||
fprintf(stderr, "Failed to create/sign note event\n");
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
id_item = cJSON_GetObjectItem(note_event, "id");
|
||||
if (id_item && cJSON_IsString(id_item)) {
|
||||
printf("Created event id: %s\n", cJSON_GetStringValue(id_item));
|
||||
}
|
||||
|
||||
printf("Publishing to %s ...\n", DEFAULT_RELAY);
|
||||
results = synchronous_publish_event_with_progress(
|
||||
relay_urls,
|
||||
1,
|
||||
note_event,
|
||||
&success_count,
|
||||
12,
|
||||
publish_progress_callback,
|
||||
NULL,
|
||||
0,
|
||||
NULL
|
||||
);
|
||||
|
||||
if (!results || success_count < 1 || results[0] != PUBLISH_SUCCESS) {
|
||||
fprintf(stderr, "Publish failed (success_count=%d, result=%d)\n",
|
||||
success_count,
|
||||
results ? (int)results[0] : -999);
|
||||
if (results) {
|
||||
free(results);
|
||||
}
|
||||
cJSON_Delete(note_event);
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("✅ Published successfully to %s\n", DEFAULT_RELAY);
|
||||
|
||||
free(results);
|
||||
cJSON_Delete(note_event);
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 0;
|
||||
}
|
||||
@@ -1,644 +0,0 @@
|
||||
/*
|
||||
* OpenTimestamps Command-Line Tool
|
||||
*
|
||||
* A simple tool for timestamping files with OpenTimestamps.
|
||||
*
|
||||
* Usage:
|
||||
* ./ots_tool stamp Interactive: type text, save to file, submit to OTS
|
||||
* ./ots_tool stamp <file> Timestamp an existing file
|
||||
* ./ots_tool check <file.ots> Check if a proof is complete
|
||||
* ./ots_tool verify <file> <file.ots> Verify a proof against a file
|
||||
* ./ots_tool upgrade <file.ots> Upgrade a pending proof
|
||||
*
|
||||
* The "stamp" command:
|
||||
* 1. If no file argument, prompts for text input and saves it to a .txt file
|
||||
* 2. Computes the SHA-256 hash of the file
|
||||
* 3. Submits the hash to an OpenTimestamps calendar
|
||||
* 4. Saves the initial .ots proof to <file>.ots
|
||||
* 5. Polls every 60 seconds to check if the proof is complete
|
||||
* 6. When complete, saves the upgraded proof and verifies it
|
||||
*
|
||||
* Options:
|
||||
* --calendar <url> Use a specific OTS calendar (default: https://alice.btc.calendar.opentimestamps.org)
|
||||
* --interval <sec> Poll interval in seconds (default: 60)
|
||||
* --max-wait <min> Maximum wait time in minutes (default: 120 = 2 hours)
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#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>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define DEFAULT_CALENDAR "https://alice.btc.calendar.opentimestamps.org"
|
||||
#define DEFAULT_INTERVAL_SEC 60
|
||||
#define DEFAULT_MAX_WAIT_MIN 120
|
||||
|
||||
/* Read an entire file into a malloc'd buffer. Returns NULL on error. */
|
||||
static unsigned char* read_file(const char* path, size_t* len_out) {
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (!f) return NULL;
|
||||
|
||||
fseek(f, 0, SEEK_END);
|
||||
long size = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
|
||||
if (size < 0) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
unsigned char* buf = (unsigned char*)malloc(size + 1);
|
||||
if (!buf) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t nread = fread(buf, 1, size, f);
|
||||
fclose(f);
|
||||
|
||||
if (nread != (size_t)size) {
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
buf[size] = '\0';
|
||||
*len_out = (size_t)size;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Write data to a file. Returns 0 on success, -1 on error. */
|
||||
static int write_file(const char* path, const unsigned char* data, size_t len) {
|
||||
FILE* f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
|
||||
size_t nwritten = fwrite(data, 1, len, f);
|
||||
fclose(f);
|
||||
|
||||
return (nwritten == len) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Compute SHA-256 of a file and return as hex string (64 chars + null). */
|
||||
static int compute_file_sha256_hex(const char* path, char* hex_out) {
|
||||
size_t file_len = 0;
|
||||
unsigned char* file_data = read_file(path, &file_len);
|
||||
if (!file_data) return -1;
|
||||
|
||||
unsigned char digest[32];
|
||||
int rc = nostr_sha256(file_data, file_len, digest);
|
||||
free(file_data);
|
||||
|
||||
if (rc != NOSTR_SUCCESS) return -1;
|
||||
|
||||
nostr_bytes_to_hex(digest, 32, hex_out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Convert base64 string to binary file and save. */
|
||||
static int save_base64_to_file(const char* path, const char* b64) {
|
||||
if (!b64) return -1;
|
||||
|
||||
size_t b64_len = strlen(b64);
|
||||
size_t max_decoded = (b64_len / 4) * 3 + 3;
|
||||
unsigned char* data = (unsigned char*)malloc(max_decoded);
|
||||
if (!data) return -1;
|
||||
|
||||
size_t data_len = base64_decode(b64, data);
|
||||
if (data_len == 0) {
|
||||
free(data);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int rc = write_file(path, data, data_len);
|
||||
free(data);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Read a binary file and convert to base64 string. Caller must free. */
|
||||
static char* read_file_as_base64(const char* path) {
|
||||
size_t file_len = 0;
|
||||
unsigned char* file_data = read_file(path, &file_len);
|
||||
if (!file_data) return NULL;
|
||||
|
||||
size_t b64_size = ((file_len + 2) / 3) * 4 + 1;
|
||||
char* b64 = (char*)malloc(b64_size);
|
||||
if (!b64) {
|
||||
free(file_data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
base64_encode(file_data, file_len, b64, b64_size);
|
||||
free(file_data);
|
||||
return b64;
|
||||
}
|
||||
|
||||
static void print_usage(const char* prog) {
|
||||
printf("OpenTimestamps Command-Line Tool\n");
|
||||
printf("\n");
|
||||
printf("Usage:\n");
|
||||
printf(" %s stamp Interactive: type text, save, submit to OTS\n", prog);
|
||||
printf(" %s stamp <file> Timestamp an existing file\n", prog);
|
||||
printf(" %s check <file.ots> Check if a proof is complete\n", prog);
|
||||
printf(" %s verify <file> <file.ots> Verify a proof against a file\n", prog);
|
||||
printf(" %s upgrade <file.ots> [file] Upgrade a pending proof (optional: original file)\n", prog);
|
||||
printf("\n");
|
||||
printf("Options:\n");
|
||||
printf(" --calendar <url> OTS calendar URL (default: %s)\n", DEFAULT_CALENDAR);
|
||||
printf(" --interval <sec> Poll interval in seconds (default: %d)\n", DEFAULT_INTERVAL_SEC);
|
||||
printf(" --max-wait <min> Max wait time in minutes (default: %d)\n", DEFAULT_MAX_WAIT_MIN);
|
||||
printf("\n");
|
||||
printf("Examples:\n");
|
||||
printf(" %s stamp # Type text, timestamp it\n", prog);
|
||||
printf(" %s stamp mydocument.txt # Timestamp a file\n", prog);
|
||||
printf(" %s check mydocument.txt.ots # Check proof status\n", prog);
|
||||
printf(" %s verify mydocument.txt mydocument.txt.ots # Verify proof\n", prog);
|
||||
}
|
||||
|
||||
static int do_stamp(const char* file_path, const char* calendar_url,
|
||||
int interval_sec, int max_wait_min) {
|
||||
char auto_file[256] = {0};
|
||||
const char* target_file = file_path;
|
||||
|
||||
/* If no file specified, prompt for text input */
|
||||
if (!target_file) {
|
||||
printf("📝 Enter text to timestamp (press Enter twice to finish):\n\n");
|
||||
|
||||
/* Read multi-line input until empty line */
|
||||
char* content = NULL;
|
||||
size_t content_cap = 0;
|
||||
size_t content_len = 0;
|
||||
char line[1024];
|
||||
int empty_count = 0;
|
||||
|
||||
while (fgets(line, sizeof(line), stdin)) {
|
||||
if (line[0] == '\n') {
|
||||
empty_count++;
|
||||
if (empty_count >= 1 && content_len > 0) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
empty_count = 0;
|
||||
}
|
||||
|
||||
size_t line_len = strlen(line);
|
||||
if (content_len + line_len + 1 > content_cap) {
|
||||
content_cap = (content_len + line_len + 1) * 2;
|
||||
char* new_content = (char*)realloc(content, content_cap);
|
||||
if (!new_content) {
|
||||
free(content);
|
||||
printf("❌ Out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
content = new_content;
|
||||
}
|
||||
memcpy(content + content_len, line, line_len);
|
||||
content_len += line_len;
|
||||
content[content_len] = '\0';
|
||||
}
|
||||
|
||||
if (!content || content_len == 0) {
|
||||
printf("❌ No text entered.\n");
|
||||
free(content);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Generate a filename based on timestamp */
|
||||
time_t now = time(NULL);
|
||||
struct tm* tm_info = localtime(&now);
|
||||
strftime(auto_file, sizeof(auto_file), "timestamped_%Y%m%d_%H%M%S.txt", tm_info);
|
||||
|
||||
/* Save the text to a file */
|
||||
if (write_file(auto_file, (const unsigned char*)content, content_len) != 0) {
|
||||
printf("❌ Failed to save text to file: %s\n", auto_file);
|
||||
free(content);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("\n💾 Saved text to: %s (%zu bytes)\n", auto_file, content_len);
|
||||
free(content);
|
||||
target_file = auto_file;
|
||||
}
|
||||
|
||||
/* Compute SHA-256 of the file */
|
||||
char digest_hex[65];
|
||||
if (compute_file_sha256_hex(target_file, digest_hex) != 0) {
|
||||
printf("❌ Failed to compute SHA-256 of file: %s\n", target_file);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("🔐 SHA-256: %s\n", digest_hex);
|
||||
|
||||
/* Submit to multiple OTS calendars for redundancy */
|
||||
const char* default_calendars[] = {
|
||||
"https://alice.btc.calendar.opentimestamps.org",
|
||||
"https://bob.btc.calendar.opentimestamps.org",
|
||||
"https://finney.calendar.eternitywall.com",
|
||||
"https://btc.calendar.catallaxy.com",
|
||||
};
|
||||
int num_calendars = 4;
|
||||
|
||||
/* If a specific calendar was specified, use only that one */
|
||||
const char** calendars_to_use;
|
||||
int num_to_use;
|
||||
if (strcmp(calendar_url, DEFAULT_CALENDAR) != 0) {
|
||||
calendars_to_use = &calendar_url;
|
||||
num_to_use = 1;
|
||||
} else {
|
||||
calendars_to_use = default_calendars;
|
||||
num_to_use = num_calendars;
|
||||
}
|
||||
|
||||
printf("\n📤 Submitting to %d OpenTimestamps calendar(s)...\n", num_to_use);
|
||||
|
||||
/* Submit to all calendars and create a combined DetachedTimestampFile */
|
||||
char* ots_b64 = nostr_nip03_stamp_with_multiple_calendars(
|
||||
digest_hex, calendars_to_use, num_to_use, 30);
|
||||
if (!ots_b64) {
|
||||
printf("❌ Failed to submit to any calendar.\n");
|
||||
printf(" Check your internet connection and try again.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Save the .ots proof */
|
||||
char ots_path[512];
|
||||
snprintf(ots_path, sizeof(ots_path), "%s.ots", target_file);
|
||||
|
||||
if (save_base64_to_file(ots_path, ots_b64) != 0) {
|
||||
printf("❌ Failed to save .ots proof to: %s\n", ots_path);
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("✅ Proof saved to: %s\n", ots_path);
|
||||
|
||||
/* Show attestation info - list all pending calendars */
|
||||
char** pending_uris = NULL;
|
||||
int pending_count = nostr_nip03_get_pending_uris(ots_b64, &pending_uris, 16);
|
||||
if (pending_count > 0) {
|
||||
printf(" Pending at %d calendar(s):\n", pending_count);
|
||||
for (int i = 0; i < pending_count; i++) {
|
||||
printf(" • %s\n", pending_uris[i]);
|
||||
free(pending_uris[i]);
|
||||
}
|
||||
free(pending_uris);
|
||||
}
|
||||
|
||||
/* Check if already complete (unlikely but possible) */
|
||||
int complete = nostr_nip03_is_proof_complete(ots_b64);
|
||||
if (complete == 1) {
|
||||
printf("\n🎉 Proof is already complete!\n");
|
||||
free(ots_b64);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Poll for completion */
|
||||
int max_attempts = (max_wait_min * 60) / interval_sec;
|
||||
printf("\n⏳ Waiting for Bitcoin attestation...\n");
|
||||
printf(" Polling every %d seconds (max %d minutes)\n", interval_sec, max_wait_min);
|
||||
printf(" Press Ctrl+C to stop. The .ots file can be checked later with 'check'.\n\n");
|
||||
|
||||
int attempts = 0;
|
||||
while (!complete && attempts < max_attempts) {
|
||||
attempts++;
|
||||
|
||||
time_t now = time(NULL);
|
||||
char time_str[26];
|
||||
ctime_r(&now, time_str);
|
||||
time_str[24] = '\0';
|
||||
|
||||
printf("[%s] Attempt %d/%d: %s\n", time_str, attempts, max_attempts,
|
||||
complete ? "✅ COMPLETE" : "⏳ PENDING");
|
||||
fflush(stdout);
|
||||
|
||||
if (complete) break;
|
||||
|
||||
sleep(interval_sec);
|
||||
|
||||
/* Try to upgrade the proof by walking the tree and fetching upgraded sub-proofs */
|
||||
char* upgraded = nostr_nip03_upgrade_proof_tree(ots_b64, 30);
|
||||
if (upgraded) {
|
||||
free(ots_b64);
|
||||
ots_b64 = upgraded;
|
||||
|
||||
/* Save the upgraded proof */
|
||||
save_base64_to_file(ots_path, ots_b64);
|
||||
|
||||
/* Check if now complete */
|
||||
complete = nostr_nip03_is_proof_complete(ots_b64);
|
||||
}
|
||||
}
|
||||
|
||||
if (complete) {
|
||||
printf("\n🎉 SUCCESS! Proof is complete with a Bitcoin attestation!\n");
|
||||
|
||||
/* Save the final proof */
|
||||
save_base64_to_file(ots_path, ots_b64);
|
||||
printf(" Final proof saved to: %s\n", ots_path);
|
||||
|
||||
/* Show attestation details */
|
||||
int has_bitcoin = 0;
|
||||
uint64_t btc_height = 0;
|
||||
if (nostr_nip03_get_attestation_info(ots_b64, &has_bitcoin, NULL, NULL,
|
||||
&btc_height, NULL, NULL) == 0) {
|
||||
if (has_bitcoin) {
|
||||
printf(" Bitcoin block height: %llu\n", (unsigned long long)btc_height);
|
||||
}
|
||||
}
|
||||
|
||||
/* Verify the proof */
|
||||
uint64_t verify_height = 0;
|
||||
int verify_rc = nostr_nip03_verify_proof(ots_b64, digest_hex, &verify_height, NULL);
|
||||
if (verify_rc == 0) {
|
||||
printf(" ✅ Verified: file digest matches, Bitcoin height=%llu\n",
|
||||
(unsigned long long)verify_height);
|
||||
} else {
|
||||
printf(" ⚠️ Verification returned: %d\n", verify_rc);
|
||||
}
|
||||
} else {
|
||||
printf("\n⏳ Proof is still pending after %d minutes.\n", max_wait_min);
|
||||
printf(" The .ots file has been saved: %s\n", ots_path);
|
||||
printf(" Check again later with: ./ots_tool check %s\n", ots_path);
|
||||
printf(" Or upgrade with: ./ots_tool upgrade %s\n", ots_path);
|
||||
}
|
||||
|
||||
free(ots_b64);
|
||||
return complete ? 0 : 2;
|
||||
}
|
||||
|
||||
static int do_check(const char* ots_path) {
|
||||
printf("🔎 Checking proof: %s\n\n", ots_path);
|
||||
|
||||
char* ots_b64 = read_file_as_base64(ots_path);
|
||||
if (!ots_b64) {
|
||||
printf("❌ Failed to read .ots file: %s\n", ots_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int complete = nostr_nip03_is_proof_complete(ots_b64);
|
||||
|
||||
if (complete == 1) {
|
||||
printf("✅ Proof is COMPLETE (has Bitcoin/Litecoin attestation)\n");
|
||||
} else if (complete == 0) {
|
||||
printf("⏳ Proof is PENDING (no Bitcoin attestation yet)\n");
|
||||
} else {
|
||||
printf("❌ Failed to parse proof (invalid OTS file?)\n");
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Show detailed attestation info */
|
||||
int has_bitcoin = 0, has_litecoin = 0, has_pending = 0;
|
||||
uint64_t btc_height = 0, ltc_height = 0;
|
||||
|
||||
if (nostr_nip03_get_attestation_info(ots_b64, &has_bitcoin, &has_litecoin, &has_pending,
|
||||
&btc_height, <c_height, NULL) == 0) {
|
||||
if (has_bitcoin) {
|
||||
printf(" ✅ Bitcoin attestation: block height %llu\n", (unsigned long long)btc_height);
|
||||
}
|
||||
if (has_litecoin) {
|
||||
printf(" ✅ Litecoin attestation: block height %llu\n", (unsigned long long)ltc_height);
|
||||
}
|
||||
}
|
||||
|
||||
/* Show all pending calendar URIs */
|
||||
if (has_pending) {
|
||||
char** pending_uris = NULL;
|
||||
int pending_count = nostr_nip03_get_pending_uris(ots_b64, &pending_uris, 16);
|
||||
if (pending_count > 0) {
|
||||
printf(" ⏳ Pending at %d calendar(s):\n", pending_count);
|
||||
for (int i = 0; i < pending_count; i++) {
|
||||
printf(" • %s\n", pending_uris[i]);
|
||||
free(pending_uris[i]);
|
||||
}
|
||||
free(pending_uris);
|
||||
}
|
||||
}
|
||||
|
||||
free(ots_b64);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int do_verify(const char* file_path, const char* ots_path) {
|
||||
printf("🔍 Verifying proof...\n");
|
||||
printf(" File: %s\n", file_path);
|
||||
printf(" Proof: %s\n\n", ots_path);
|
||||
|
||||
/* Compute SHA-256 of the file */
|
||||
char digest_hex[65];
|
||||
if (compute_file_sha256_hex(file_path, digest_hex) != 0) {
|
||||
printf("❌ Failed to compute SHA-256 of file: %s\n", file_path);
|
||||
return 1;
|
||||
}
|
||||
printf(" File SHA-256: %s\n", digest_hex);
|
||||
|
||||
/* Read the .ots file */
|
||||
char* ots_b64 = read_file_as_base64(ots_path);
|
||||
if (!ots_b64) {
|
||||
printf("❌ Failed to read .ots file: %s\n", ots_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Verify the proof */
|
||||
uint64_t block_height = 0;
|
||||
time_t block_time = 0;
|
||||
int rc = nostr_nip03_verify_proof(ots_b64, digest_hex, &block_height, &block_time);
|
||||
|
||||
switch (rc) {
|
||||
case 0:
|
||||
printf("\n✅ Proof VERIFIED!\n");
|
||||
printf(" Bitcoin block height: %llu\n", (unsigned long long)block_height);
|
||||
printf(" The file's hash is committed in the Bitcoin blockchain.\n");
|
||||
free(ots_b64);
|
||||
return 0;
|
||||
|
||||
case 1:
|
||||
printf("\n⏳ Proof is valid but still PENDING (no Bitcoin attestation yet).\n");
|
||||
printf(" The file digest matches the proof.\n");
|
||||
printf(" Wait for the calendar to commit to the blockchain, then run:\n");
|
||||
printf(" ./ots_tool upgrade %s\n", ots_path);
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
|
||||
case -1:
|
||||
printf("\n❌ Failed to parse OTS proof. The .ots file may be corrupted.\n");
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
|
||||
case -2:
|
||||
printf("\n❌ DIGEST MISMATCH! The proof does not match this file.\n");
|
||||
printf(" The file may have been modified after timestamping.\n");
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
|
||||
default:
|
||||
printf("\n❌ Unknown error: %d\n", rc);
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
static int do_upgrade(const char* ots_path, const char* file_path,
|
||||
const char* calendar_url) {
|
||||
printf("⬆️ Upgrading proof: %s\n\n", ots_path);
|
||||
|
||||
char* ots_b64 = read_file_as_base64(ots_path);
|
||||
if (!ots_b64) {
|
||||
printf("❌ Failed to read .ots file: %s\n", ots_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Check current status */
|
||||
int was_complete = nostr_nip03_is_proof_complete(ots_b64);
|
||||
if (was_complete == 1) {
|
||||
printf("✅ Proof is already complete! No upgrade needed.\n");
|
||||
free(ots_b64);
|
||||
return 0;
|
||||
}
|
||||
|
||||
(void)calendar_url; /* upgrade now uses pending-attestation URIs from proof */
|
||||
(void)file_path;
|
||||
|
||||
printf("📤 Checking pending attestations and fetching upgrades...\n");
|
||||
|
||||
char* upgraded = NULL;
|
||||
/* Use the tree-walking upgrade which correctly queries commitment hashes */
|
||||
upgraded = nostr_nip03_upgrade_proof_tree(ots_b64, 30);
|
||||
|
||||
if (!upgraded) {
|
||||
printf("⏳ No upgrade available yet. The proof is still pending.\n");
|
||||
printf(" Bitcoin attestations typically take 1-12 hours.\n");
|
||||
if (!file_path) {
|
||||
printf(" Tip: provide the original file path to enable digest-based upgrade:\n");
|
||||
printf(" ./ots_tool upgrade %s <original-file>\n", ots_path);
|
||||
}
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Save the upgraded proof */
|
||||
if (save_base64_to_file(ots_path, upgraded) != 0) {
|
||||
printf("❌ Failed to save upgraded proof.\n");
|
||||
free(ots_b64);
|
||||
free(upgraded);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Check if now complete */
|
||||
int complete = nostr_nip03_is_proof_complete(upgraded);
|
||||
if (complete == 1) {
|
||||
printf("✅ Upgrade successful! Proof is now COMPLETE with a Bitcoin attestation.\n");
|
||||
|
||||
int has_bitcoin = 0;
|
||||
uint64_t btc_height = 0;
|
||||
if (nostr_nip03_get_attestation_info(upgraded, &has_bitcoin, NULL, NULL,
|
||||
&btc_height, NULL, NULL) == 0) {
|
||||
if (has_bitcoin) {
|
||||
printf(" Bitcoin block height: %llu\n", (unsigned long long)btc_height);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printf("⏳ Upgrade received but proof is still pending.\n");
|
||||
printf(" The upgraded proof has been saved. Try again later.\n");
|
||||
}
|
||||
|
||||
printf(" Updated proof saved to: %s\n", ots_path);
|
||||
|
||||
free(ots_b64);
|
||||
free(upgraded);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to initialize NOSTR library\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* calendar_url = DEFAULT_CALENDAR;
|
||||
int interval_sec = DEFAULT_INTERVAL_SEC;
|
||||
int max_wait_min = DEFAULT_MAX_WAIT_MIN;
|
||||
|
||||
enum { MODE_NONE, MODE_STAMP, MODE_CHECK, MODE_VERIFY, MODE_UPGRADE } mode = MODE_NONE;
|
||||
const char* arg1 = NULL;
|
||||
const char* arg2 = NULL;
|
||||
|
||||
/* Parse arguments */
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
|
||||
print_usage(argv[0]);
|
||||
nostr_cleanup();
|
||||
return 0;
|
||||
} else if (strcmp(argv[i], "--calendar") == 0 && i + 1 < argc) {
|
||||
calendar_url = argv[++i];
|
||||
} else if (strcmp(argv[i], "--interval") == 0 && i + 1 < argc) {
|
||||
interval_sec = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--max-wait") == 0 && i + 1 < argc) {
|
||||
max_wait_min = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "stamp") == 0) {
|
||||
mode = MODE_STAMP;
|
||||
} else if (strcmp(argv[i], "check") == 0) {
|
||||
mode = MODE_CHECK;
|
||||
} else if (strcmp(argv[i], "verify") == 0) {
|
||||
mode = MODE_VERIFY;
|
||||
} else if (strcmp(argv[i], "upgrade") == 0) {
|
||||
mode = MODE_UPGRADE;
|
||||
} else if (argv[i][0] != '-') {
|
||||
if (!arg1) {
|
||||
arg1 = argv[i];
|
||||
} else if (!arg2) {
|
||||
arg2 = argv[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int rc = 0;
|
||||
|
||||
switch (mode) {
|
||||
case MODE_STAMP:
|
||||
rc = do_stamp(arg1, calendar_url, interval_sec, max_wait_min);
|
||||
break;
|
||||
|
||||
case MODE_CHECK:
|
||||
if (!arg1) {
|
||||
printf("❌ Missing .ots file path\n");
|
||||
print_usage(argv[0]);
|
||||
rc = 1;
|
||||
} else {
|
||||
rc = do_check(arg1);
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_VERIFY:
|
||||
if (!arg1 || !arg2) {
|
||||
printf("❌ Missing file or .ots path\n");
|
||||
print_usage(argv[0]);
|
||||
rc = 1;
|
||||
} else {
|
||||
rc = do_verify(arg1, arg2);
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_UPGRADE:
|
||||
if (!arg1) {
|
||||
printf("❌ Missing .ots file path\n");
|
||||
print_usage(argv[0]);
|
||||
rc = 1;
|
||||
} else {
|
||||
rc = do_upgrade(arg1, arg2, calendar_url);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
print_usage(argv[0]);
|
||||
rc = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -187,8 +187,7 @@ int main(int argc, char* argv[]) {
|
||||
1,
|
||||
sender_privkey,
|
||||
gift_wraps,
|
||||
10,
|
||||
0
|
||||
10
|
||||
);
|
||||
|
||||
cJSON_Delete(dm_event); // Original DM event no longer needed
|
||||
|
||||
Binary file not shown.
@@ -1,328 +0,0 @@
|
||||
/*
|
||||
* NIP-03 OpenTimestamps Example
|
||||
*
|
||||
* This example demonstrates the full NIP-03 timestamping workflow:
|
||||
* 1. Create a Nostr event
|
||||
* 2. Submit the event ID to an OpenTimestamps calendar
|
||||
* 3. Poll for proof completion (Bitcoin attestation)
|
||||
* 4. Create a NIP-03 proof event (kind 1040)
|
||||
* 5. Verify the proof
|
||||
*
|
||||
* Usage:
|
||||
* ./timestamping # Create and timestamp a test event
|
||||
* ./timestamping <event_id> # Timestamp an existing event ID
|
||||
* ./timestamping --verify <ots_b64> # Verify a proof
|
||||
*
|
||||
* Note: Bitcoin attestations typically take 1-12 hours to appear.
|
||||
* The OpenTimestamps calendars commit to the Bitcoin blockchain
|
||||
* periodically, so you may need to wait and poll for completion.
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#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 DEFAULT_CALENDAR_URL "https://alice.btc.calendar.opentimestamps.org"
|
||||
#define POLL_INTERVAL_SEC 300 /* 5 minutes */
|
||||
#define MAX_POLL_ATTEMPTS 144 /* 12 hours total */
|
||||
|
||||
static void print_usage(const char* prog_name) {
|
||||
printf("NIP-03 OpenTimestamps Example\n");
|
||||
printf("\n");
|
||||
printf("Usage:\n");
|
||||
printf(" %s Create and timestamp a test event\n", prog_name);
|
||||
printf(" %s <event_id> Timestamp an existing event ID (64-char hex)\n", prog_name);
|
||||
printf(" %s --verify <ots_b64> Verify a base64-encoded OTS proof\n", prog_name);
|
||||
printf(" %s --check <ots_b64> Check if a proof is complete\n", prog_name);
|
||||
printf("\n");
|
||||
printf("Options:\n");
|
||||
printf(" --calendar <url> Use a specific OTS calendar URL\n");
|
||||
printf(" --interval <seconds> Poll interval in seconds (default: %d)\n", POLL_INTERVAL_SEC);
|
||||
printf(" --max-attempts <n> Max poll attempts (default: %d)\n", MAX_POLL_ATTEMPTS);
|
||||
printf("\n");
|
||||
printf("Common calendar URLs:\n");
|
||||
printf(" https://alice.btc.calendar.opentimestamps.org\n");
|
||||
printf(" https://bob.btc.calendar.opentimestamps.org\n");
|
||||
printf(" https://finney.calendar.eternitywall.com\n");
|
||||
}
|
||||
|
||||
static void print_attestation_info(const char* ots_b64) {
|
||||
int has_bitcoin = 0, has_litecoin = 0, has_pending = 0;
|
||||
uint64_t btc_height = 0, ltc_height = 0;
|
||||
char* pending_uri = NULL;
|
||||
|
||||
if (nostr_nip03_get_attestation_info(ots_b64, &has_bitcoin, &has_litecoin, &has_pending,
|
||||
&btc_height, <c_height, &pending_uri) != 0) {
|
||||
printf(" ⚠️ Failed to parse attestation info\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (has_bitcoin) {
|
||||
printf(" ✅ Bitcoin attestation: block height %llu\n", (unsigned long long)btc_height);
|
||||
}
|
||||
if (has_litecoin) {
|
||||
printf(" ✅ Litecoin attestation: block height %llu\n", (unsigned long long)ltc_height);
|
||||
}
|
||||
if (has_pending) {
|
||||
if (pending_uri) {
|
||||
printf(" ⏳ Pending at: %s\n", pending_uri);
|
||||
free(pending_uri);
|
||||
}
|
||||
}
|
||||
if (!has_bitcoin && !has_litecoin && !has_pending) {
|
||||
printf(" ⚠️ No attestations found\n");
|
||||
}
|
||||
}
|
||||
|
||||
static int do_timestamp(const char* event_id_hex, const char* calendar_url,
|
||||
int poll_interval, int max_attempts) {
|
||||
printf("📅 NIP-03 Timestamping\n");
|
||||
printf(" Event ID: %s\n", event_id_hex);
|
||||
printf(" Calendar: %s\n", calendar_url);
|
||||
printf("\n");
|
||||
|
||||
/* Step 1: Submit to calendar */
|
||||
printf("📤 Submitting to OpenTimestamps calendar...\n");
|
||||
char* ots_b64 = nostr_nip03_request_timestamp(event_id_hex, calendar_url, 30);
|
||||
if (!ots_b64) {
|
||||
printf("❌ Failed to submit to calendar.\n");
|
||||
printf(" Check your internet connection and the calendar URL.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("✅ Initial proof received.\n");
|
||||
print_attestation_info(ots_b64);
|
||||
printf("\n");
|
||||
|
||||
/* Step 2: Poll for completion */
|
||||
printf("⏳ Polling for Bitcoin attestation (every %d seconds, max %d attempts)...\n",
|
||||
poll_interval, max_attempts);
|
||||
|
||||
int complete = 0;
|
||||
int attempts = 0;
|
||||
|
||||
while (!complete && attempts < max_attempts) {
|
||||
attempts++;
|
||||
complete = nostr_nip03_is_proof_complete(ots_b64);
|
||||
|
||||
time_t now = time(NULL);
|
||||
char time_str[26];
|
||||
ctime_r(&now, time_str);
|
||||
time_str[24] = '\0';
|
||||
|
||||
printf("[%s] Attempt %d/%d: %s\n", time_str, attempts, max_attempts,
|
||||
complete ? "✅ COMPLETE" : "⏳ PENDING");
|
||||
|
||||
if (!complete) {
|
||||
sleep(poll_interval);
|
||||
|
||||
/* Try to upgrade the proof */
|
||||
char* upgraded = nostr_nip03_upgrade_proof(ots_b64, calendar_url, 30);
|
||||
if (upgraded) {
|
||||
free(ots_b64);
|
||||
ots_b64 = upgraded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (complete) {
|
||||
printf("\n🎉 SUCCESS! The proof is now complete with a Bitcoin attestation.\n");
|
||||
print_attestation_info(ots_b64);
|
||||
|
||||
/* Verify the proof */
|
||||
uint64_t block_height = 0;
|
||||
int verify_rc = nostr_nip03_verify_proof(ots_b64, event_id_hex, &block_height, NULL);
|
||||
if (verify_rc == 0) {
|
||||
printf(" ✅ Proof verified: digest matches, Bitcoin height=%llu\n",
|
||||
(unsigned long long)block_height);
|
||||
} else {
|
||||
printf(" ⚠️ Proof verification returned: %d\n", verify_rc);
|
||||
}
|
||||
|
||||
/* Create the NIP-03 proof event */
|
||||
unsigned char private_key[32];
|
||||
unsigned char public_key[32];
|
||||
nostr_generate_keypair(private_key, public_key);
|
||||
|
||||
cJSON* proof_event = nostr_nip03_create_proof_event(event_id_hex, 1, ots_b64, NULL, private_key);
|
||||
if (proof_event) {
|
||||
char* json = cJSON_Print(proof_event);
|
||||
printf("\n📄 NIP-03 Proof Event (kind 1040):\n%s\n", json);
|
||||
free(json);
|
||||
cJSON_Delete(proof_event);
|
||||
}
|
||||
|
||||
/* Print the base64 OTS data for storage */
|
||||
printf("\n📦 OTS proof (base64, save this):\n%s\n", ots_b64);
|
||||
} else {
|
||||
printf("\n⏳ Proof is still pending after %d attempts.\n", attempts);
|
||||
printf(" Bitcoin attestations can take several hours.\n");
|
||||
printf(" Save the OTS proof and check again later:\n%s\n", ots_b64);
|
||||
}
|
||||
|
||||
free(ots_b64);
|
||||
return complete ? 0 : 2;
|
||||
}
|
||||
|
||||
static int do_verify(const char* ots_b64, const char* target_digest_hex) {
|
||||
printf("🔍 Verifying OTS proof...\n\n");
|
||||
|
||||
uint64_t block_height = 0;
|
||||
time_t block_time = 0;
|
||||
|
||||
int rc = nostr_nip03_verify_proof(ots_b64, target_digest_hex, &block_height, &block_time);
|
||||
|
||||
switch (rc) {
|
||||
case 0:
|
||||
printf("✅ Proof verified!\n");
|
||||
printf(" Bitcoin block height: %llu\n", (unsigned long long)block_height);
|
||||
print_attestation_info(ots_b64);
|
||||
return 0;
|
||||
case 1:
|
||||
printf("⏳ Proof is valid but has no Bitcoin attestation (still pending).\n");
|
||||
print_attestation_info(ots_b64);
|
||||
return 1;
|
||||
case -1:
|
||||
printf("❌ Failed to parse OTS proof.\n");
|
||||
return 1;
|
||||
case -2:
|
||||
printf("❌ Digest mismatch! The proof does not match the expected digest.\n");
|
||||
return 1;
|
||||
default:
|
||||
printf("❌ Unknown error: %d\n", rc);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
static int do_check(const char* ots_b64) {
|
||||
printf("🔎 Checking OTS proof status...\n\n");
|
||||
|
||||
int complete = nostr_nip03_is_proof_complete(ots_b64);
|
||||
|
||||
if (complete == 1) {
|
||||
printf("✅ Proof is COMPLETE (has Bitcoin/Litecoin attestation)\n");
|
||||
} else if (complete == 0) {
|
||||
printf("⏳ Proof is PENDING (no Bitcoin attestation yet)\n");
|
||||
} else {
|
||||
printf("❌ Failed to parse proof (error)\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
print_attestation_info(ots_b64);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to initialize NOSTR library\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* calendar_url = DEFAULT_CALENDAR_URL;
|
||||
int poll_interval = POLL_INTERVAL_SEC;
|
||||
int max_attempts = MAX_POLL_ATTEMPTS;
|
||||
const char* event_id = NULL;
|
||||
const char* ots_b64 = NULL;
|
||||
const char* verify_digest = NULL;
|
||||
|
||||
enum { MODE_TIMESTAMP, MODE_VERIFY, MODE_CHECK } mode = MODE_TIMESTAMP;
|
||||
|
||||
/* Parse arguments */
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
|
||||
print_usage(argv[0]);
|
||||
nostr_cleanup();
|
||||
return 0;
|
||||
} else if (strcmp(argv[i], "--verify") == 0) {
|
||||
mode = MODE_VERIFY;
|
||||
/* OTS data is provided as a positional argument */
|
||||
} else if (strcmp(argv[i], "--check") == 0) {
|
||||
mode = MODE_CHECK;
|
||||
/* OTS data is provided as a positional argument */
|
||||
} else if (strcmp(argv[i], "--calendar") == 0 && i + 1 < argc) {
|
||||
calendar_url = argv[++i];
|
||||
} else if (strcmp(argv[i], "--interval") == 0 && i + 1 < argc) {
|
||||
poll_interval = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--max-attempts") == 0 && i + 1 < argc) {
|
||||
max_attempts = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--digest") == 0 && i + 1 < argc) {
|
||||
verify_digest = argv[++i];
|
||||
} else if (argv[i][0] != '-' && !event_id && !ots_b64) {
|
||||
if (mode == MODE_VERIFY || mode == MODE_CHECK) {
|
||||
if (!ots_b64) ots_b64 = argv[i];
|
||||
} else {
|
||||
event_id = argv[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int rc = 0;
|
||||
|
||||
switch (mode) {
|
||||
case MODE_TIMESTAMP:
|
||||
if (!event_id) {
|
||||
/* Create a test event */
|
||||
printf("🧪 Creating a test event to timestamp...\n");
|
||||
unsigned char private_key[32];
|
||||
unsigned char public_key[32];
|
||||
nostr_generate_keypair(private_key, public_key);
|
||||
|
||||
char content[128];
|
||||
snprintf(content, sizeof(content), "NIP-03 timestamp test at %ld", time(NULL));
|
||||
|
||||
cJSON* event = nostr_create_and_sign_event(1, content, NULL, private_key, 0);
|
||||
if (!event) {
|
||||
printf("❌ Failed to create test event\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
cJSON* id_item = cJSON_GetObjectItem(event, "id");
|
||||
if (!id_item || !cJSON_IsString(id_item)) {
|
||||
printf("❌ Failed to get event ID\n");
|
||||
cJSON_Delete(event);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
char* event_json = cJSON_Print(event);
|
||||
printf("📄 Test event:\n%s\n\n", event_json);
|
||||
free(event_json);
|
||||
|
||||
rc = do_timestamp(id_item->valuestring, calendar_url, poll_interval, max_attempts);
|
||||
cJSON_Delete(event);
|
||||
} else {
|
||||
rc = do_timestamp(event_id, calendar_url, poll_interval, max_attempts);
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_VERIFY:
|
||||
if (!ots_b64) {
|
||||
printf("❌ Missing OTS proof (base64)\n");
|
||||
print_usage(argv[0]);
|
||||
rc = 1;
|
||||
} else {
|
||||
rc = do_verify(ots_b64, verify_digest);
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_CHECK:
|
||||
if (!ots_b64) {
|
||||
printf("❌ Missing OTS proof (base64)\n");
|
||||
print_usage(argv[0]);
|
||||
rc = 1;
|
||||
} else {
|
||||
rc = do_check(ots_b64);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
+14
-46
@@ -1,9 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# increment_and_push.sh - Version increment and git automation script
|
||||
# Usage:
|
||||
# ./increment_and_push.sh "meaningful git comment"
|
||||
# ./increment_and_push.sh --set-version vX.Y.Z "meaningful git comment"
|
||||
# Usage: ./increment_and_push.sh "meaningful git comment"
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
@@ -67,31 +65,17 @@ if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET_VERSION=""
|
||||
COMMIT_MESSAGE=""
|
||||
|
||||
# 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"
|
||||
# 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
|
||||
|
||||
COMMIT_MESSAGE="$1"
|
||||
|
||||
# Check if nostr_core.h exists
|
||||
if [ ! -f "nostr_core/nostr_core.h" ]; then
|
||||
print_error "nostr_core/nostr_core.h not found"
|
||||
@@ -119,37 +103,21 @@ fi
|
||||
|
||||
print_info "Current version: $CURRENT_VERSION (Major: $VERSION_MAJOR, Minor: $VERSION_MINOR, Patch: $VERSION_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
|
||||
# Increment patch version
|
||||
NEW_PATCH=$((VERSION_PATCH + 1))
|
||||
NEW_VERSION="v$VERSION_MAJOR.$VERSION_MINOR.$NEW_PATCH"
|
||||
|
||||
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 "$NEW_MAJOR.$NEW_MINOR.$NEW_PATCH" > VERSION
|
||||
echo "$VERSION_MAJOR.$VERSION_MINOR.$NEW_PATCH" > VERSION
|
||||
print_success "Updated VERSION file"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,481 +0,0 @@
|
||||
# NSIGNER Integration in `nostr_core_lib`
|
||||
|
||||
This document is the developer-facing integration contract for signer abstraction + nsigner client support in `nostr_core_lib`.
|
||||
|
||||
For the concise overview, see the signer section in `README.md`.
|
||||
For implementation planning context, see `plans/nsigner_integration_plan.md`.
|
||||
|
||||
## 1) Signer lifecycle and core verbs (exact signatures)
|
||||
|
||||
From `nostr_core/nostr_signer.h`:
|
||||
|
||||
```c
|
||||
typedef struct nostr_signer nostr_signer_t;
|
||||
|
||||
/* Lifecycle */
|
||||
nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]);
|
||||
void nostr_signer_free(nostr_signer_t* signer);
|
||||
|
||||
/* Core verbs */
|
||||
int nostr_signer_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]);
|
||||
int nostr_signer_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out);
|
||||
|
||||
/* Encryption verbs (for parity with upcoming remote backends) */
|
||||
int nostr_signer_nip04_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out);
|
||||
int nostr_signer_nip04_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out);
|
||||
int nostr_signer_nip44_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out);
|
||||
int nostr_signer_nip44_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out);
|
||||
```
|
||||
|
||||
## 2) Remote signer factories + transport API (exact signatures)
|
||||
|
||||
### 2.1 Signer-side remote factories (`nostr_core/nostr_signer.h`)
|
||||
|
||||
```c
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_serial(const char* device_path, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_tcp(const char* host, int port, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_fds(int read_fd, int write_fd, const char* role, int timeout_ms);
|
||||
/* TCP mode requires auth envelope per n_signer; set this before making calls. */
|
||||
int nostr_signer_nsigner_set_auth(nostr_signer_t* signer,
|
||||
const unsigned char auth_privkey[32],
|
||||
const char* label);
|
||||
#endif
|
||||
```
|
||||
|
||||
### 2.2 Transport constructors + framed I/O + discovery (`nostr_core/nsigner_transport.h`)
|
||||
|
||||
```c
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
typedef struct nsigner_transport nsigner_transport_t;
|
||||
|
||||
struct nsigner_transport {
|
||||
int (*send_framed)(nsigner_transport_t* t, const char* json, size_t len);
|
||||
int (*recv_framed)(nsigner_transport_t* t, char** out_json, size_t* out_len); /* caller frees *out_json */
|
||||
void (*close)(nsigner_transport_t* t);
|
||||
void* ctx;
|
||||
};
|
||||
|
||||
/* Raw framed I/O helpers for existing connected file descriptors. */
|
||||
int nsigner_transport_send_framed_fd(int fd, const char* json, size_t len);
|
||||
int nsigner_transport_recv_framed_fd(int fd, char** out_json, size_t* out_len);
|
||||
|
||||
/* UNIX abstract socket transport (socket_name without leading '@'). */
|
||||
nsigner_transport_t* nsigner_transport_open_unix(const char* socket_name, int timeout_ms);
|
||||
|
||||
/* TCP transport (host + port), with IPv4/IPv6 resolution via getaddrinfo. */
|
||||
nsigner_transport_t* nsigner_transport_open_tcp(const char* host, int port, int timeout_ms);
|
||||
|
||||
/* USB CDC-ACM serial transport (device path like /dev/ttyACM0). */
|
||||
nsigner_transport_t* nsigner_transport_open_serial(const char* device_path, int timeout_ms);
|
||||
|
||||
/*
|
||||
* Framed transport over existing file descriptors (read_fd, write_fd).
|
||||
* close() closes both owned fds. If passing STDIN_FILENO/STDOUT_FILENO and you do not
|
||||
* want them closed, pass dup()'d descriptors instead.
|
||||
*/
|
||||
nsigner_transport_t* nsigner_transport_open_fds(int read_fd, int write_fd, int timeout_ms);
|
||||
|
||||
/* Enumerate /proc/net/unix abstract sockets beginning with @nsigner, returns count copied. */
|
||||
int nsigner_transport_list_unix(char names[][64], int max_names);
|
||||
|
||||
/* Best-effort enumerate serial devices (e.g. /dev/ttyACM*), returns count copied. */
|
||||
int nsigner_transport_list_serial(char paths[][64], int max_paths);
|
||||
#endif
|
||||
```
|
||||
|
||||
## 3) Client API + auth contract
|
||||
|
||||
From `nostr_core/nsigner_client.h`:
|
||||
|
||||
```c
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
typedef struct nsigner_client nsigner_client_t;
|
||||
|
||||
nsigner_client_t* nsigner_client_new(nsigner_transport_t* transport); /* takes ownership of transport */
|
||||
void nsigner_client_free(nsigner_client_t* client);
|
||||
|
||||
int nsigner_client_set_auth(nsigner_client_t* client, const unsigned char privkey[32], const char* label);
|
||||
|
||||
int nsigner_client_call(nsigner_client_t* client,
|
||||
const char* method,
|
||||
cJSON* params,
|
||||
cJSON** out_result);
|
||||
|
||||
const char* nsigner_client_last_error(const nsigner_client_t* client);
|
||||
int nsigner_client_compute_body_hash_hex(const cJSON* params, char out_hash_hex[65]);
|
||||
#endif
|
||||
```
|
||||
|
||||
### n_signer wire methods
|
||||
|
||||
The remote signer backend uses n_signer's role-based Nostr methods:
|
||||
|
||||
- `nostr_get_public_key`
|
||||
- `nostr_sign_event`
|
||||
- `nostr_nip04_encrypt` / `nostr_nip04_decrypt`
|
||||
- `nostr_nip44_encrypt` / `nostr_nip44_decrypt`
|
||||
|
||||
These wire names are distinct from both the public `nostr_signer_*` C API and the
|
||||
unprefixed NIP-46 method names. In particular, n_signer's unprefixed `get_public_key`
|
||||
is algorithm-based; this library uses `nostr_get_public_key` because remote signer
|
||||
factories select keys by `role` or `nostr_index`.
|
||||
|
||||
### Auth and framing notes
|
||||
|
||||
- Framing is 4-byte big-endian payload length + JSON payload.
|
||||
- Valid payload lengths are `1..65536` bytes (`NSIGNER_CLIENT_MAX_FRAME` in transport implementation).
|
||||
- TCP requires auth (`nostr_signer_nsigner_set_auth`), unix auth is optional.
|
||||
- Auth envelope is a kind-27235 event containing tags:
|
||||
- `nsigner_rpc`
|
||||
- `nsigner_method`
|
||||
- `nsigner_body_hash`
|
||||
- `nsigner_body_hash` is SHA-256 of compact params JSON, or SHA-256 of literal `"null"` when params are null (`nsigner_client_compute_body_hash_hex`).
|
||||
|
||||
## 4) Signer-aware higher-level APIs (`*_with_signer`)
|
||||
|
||||
Below is the full list currently present in headers.
|
||||
|
||||
### NIP-01 (`nostr_core/nip001.h`)
|
||||
|
||||
```c
|
||||
cJSON* nostr_create_and_sign_event_with_signer(int kind, const char* content, cJSON* tags, nostr_signer_t* signer, time_t timestamp);
|
||||
```
|
||||
|
||||
### NIP-17 (`nostr_core/nip017.h`)
|
||||
|
||||
```c
|
||||
cJSON* nostr_nip17_create_relay_list_event_with_signer(const char** relay_urls,
|
||||
int num_relays,
|
||||
nostr_signer_t* signer);
|
||||
int nostr_nip17_send_dm_with_signer(cJSON* dm_event,
|
||||
const char** recipient_pubkeys,
|
||||
int num_recipients,
|
||||
nostr_signer_t* signer,
|
||||
cJSON** gift_wraps_out,
|
||||
int max_gift_wraps,
|
||||
long max_delay_sec);
|
||||
cJSON* nostr_nip17_receive_dm_with_signer(cJSON* gift_wrap,
|
||||
nostr_signer_t* signer);
|
||||
```
|
||||
|
||||
### NIP-42 (`nostr_core/nip042.h`)
|
||||
|
||||
```c
|
||||
cJSON* nostr_nip42_create_auth_event_with_signer(const char* challenge,
|
||||
const char* relay_url,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
```
|
||||
|
||||
### NIP-46 (`nostr_core/nip046.h`)
|
||||
|
||||
```c
|
||||
cJSON* nostr_nip46_create_request_event_with_signer(const nostr_nip46_request_t* request,
|
||||
nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip46_create_response_event_with_signer(const nostr_nip46_response_t* response,
|
||||
nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp);
|
||||
int nostr_nip46_decrypt_event_with_signer(cJSON* event,
|
||||
nostr_signer_t* signer,
|
||||
char** output_out);
|
||||
int nostr_nip46_client_session_init_with_signer(nostr_nip46_client_session_t* session,
|
||||
nostr_signer_t* signer,
|
||||
const char* bunker_url);
|
||||
```
|
||||
|
||||
### NIP-60 (`nostr_core/nip060.h`)
|
||||
|
||||
```c
|
||||
cJSON* nostr_nip60_create_wallet_event_with_signer(const nostr_nip60_wallet_data_t* wallet_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_token_event_with_signer(const nostr_nip60_token_data_t* token_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_token_deletion_with_signer(const char* token_event_id,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_rollover_token_with_signer(const nostr_nip60_token_data_t* remaining_proofs,
|
||||
const char** deleted_event_ids,
|
||||
int deleted_count,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_history_event_with_signer(const nostr_nip60_history_data_t* history_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_quote_event_with_signer(const char* quote_id,
|
||||
const char* mint_url,
|
||||
time_t expiration,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
```
|
||||
|
||||
### NIP-61 (`nostr_core/nip061.h`)
|
||||
|
||||
```c
|
||||
cJSON* nostr_nip61_create_nutzap_info_event_with_signer(const nostr_nip61_nutzap_info_t* info,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip61_create_nutzap_event_with_signer(const nostr_nip61_nutzap_data_t* nutzap_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip61_create_redemption_event_with_signer(const char* nutzap_event_id,
|
||||
const char* nutzap_relay_hint,
|
||||
const char* sender_pubkey,
|
||||
const char* created_token_event_id,
|
||||
const char* created_token_relay_hint,
|
||||
uint64_t amount,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
```
|
||||
|
||||
### NIP-59 (`nostr_core/nip059.h`) (additional signer coverage present)
|
||||
|
||||
```c
|
||||
cJSON* nostr_nip59_create_seal_with_signer(cJSON* rumor, nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key, long max_delay_sec);
|
||||
cJSON* nostr_nip59_unwrap_gift_with_signer(cJSON* gift_wrap, nostr_signer_t* signer);
|
||||
cJSON* nostr_nip59_unseal_rumor_with_signer(cJSON* seal, const unsigned char* sender_public_key,
|
||||
nostr_signer_t* signer);
|
||||
```
|
||||
|
||||
### Blossom client (`nostr_core/blossom_client.h`)
|
||||
|
||||
```c
|
||||
char* blossom_create_auth_header_with_signer(nostr_signer_t* signer,
|
||||
const char* operation,
|
||||
const char* sha256_hex,
|
||||
int expiration_seconds,
|
||||
time_t timestamp);
|
||||
int blossom_upload_with_signer(const char* server_url,
|
||||
const unsigned char* data,
|
||||
size_t data_len,
|
||||
const char* content_type,
|
||||
nostr_signer_t* signer,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
int blossom_upload_file_with_signer(const char* server_url,
|
||||
const char* file_path,
|
||||
const char* content_type,
|
||||
nostr_signer_t* signer,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
int blossom_delete_with_signer(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
nostr_signer_t* signer,
|
||||
int timeout_seconds);
|
||||
```
|
||||
|
||||
### Relay pool signer entry point (`nostr_core/nostr_core.h`)
|
||||
|
||||
```c
|
||||
int nostr_relay_pool_set_auth_with_signer(nostr_relay_pool_t* pool, nostr_signer_t* signer, int enable);
|
||||
```
|
||||
|
||||
## 5) Feasibility boundaries
|
||||
|
||||
Remotable through `nostr_signer_t` / nsigner client contract:
|
||||
|
||||
- Public key retrieval
|
||||
- Event signing
|
||||
- NIP-04 encrypt/decrypt
|
||||
- NIP-44 encrypt/decrypt
|
||||
|
||||
Not generally remotable without protocol/API redesign:
|
||||
|
||||
- Operations that require direct access to raw secret material for non-signer-verb cryptography.
|
||||
- In this codebase, Cashu mint-protocol-oriented pieces (e.g. in `cashu_mint`) are the identified example.
|
||||
|
||||
## 6) Minimal C usage snippets
|
||||
|
||||
### (a) Local signer create + sign + publish
|
||||
|
||||
```c
|
||||
#include "nostr_core/nostr_core.h"
|
||||
|
||||
int main(void) {
|
||||
unsigned char privkey[32];
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
const char* relays[] = {"wss://relay.laantungir.net"};
|
||||
int ok_count = 0;
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) return 1;
|
||||
|
||||
/* fill privkey via your key-management path */
|
||||
signer = nostr_signer_local(privkey);
|
||||
if (!signer) return 1;
|
||||
|
||||
evt = nostr_create_and_sign_event_with_signer(1, "hello", NULL, signer, time(NULL));
|
||||
if (!evt) return 1;
|
||||
|
||||
publish_result_t* r = synchronous_publish_event_with_progress(
|
||||
relays, 1, evt, &ok_count, 10, NULL, NULL, 0, NULL);
|
||||
|
||||
free(r);
|
||||
cJSON_Delete(evt);
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### (b) Remote unix signer create + sign
|
||||
|
||||
```c
|
||||
#include "nostr_core/nostr_core.h"
|
||||
|
||||
int main(void) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) return 1;
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
signer = nostr_signer_nsigner_unix("nsigner-main", NULL, 15000);
|
||||
if (!signer) return 1;
|
||||
|
||||
evt = nostr_create_and_sign_event_with_signer(1, "signed remotely", NULL, signer, time(NULL));
|
||||
if (!evt) return 1;
|
||||
|
||||
cJSON_Delete(evt);
|
||||
nostr_signer_free(signer);
|
||||
#endif
|
||||
|
||||
nostr_cleanup();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### (c) TCP signer + auth setup
|
||||
|
||||
```c
|
||||
#include "nostr_core/nostr_core.h"
|
||||
|
||||
int main(void) {
|
||||
nostr_signer_t* signer;
|
||||
unsigned char caller_auth_key[32];
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
signer = nostr_signer_nsigner_tcp("127.0.0.1", 7777, NULL, 15000);
|
||||
if (!signer) return 1;
|
||||
|
||||
/* Required for TCP mode */
|
||||
if (nostr_signer_nsigner_set_auth(signer, caller_auth_key, "my-caller") != NOSTR_SUCCESS) {
|
||||
nostr_signer_free(signer);
|
||||
return 1;
|
||||
}
|
||||
|
||||
nostr_signer_free(signer);
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## 7) Build/flag wiring summary (exact current lines)
|
||||
|
||||
### `build.sh`
|
||||
|
||||
```bash
|
||||
SOURCES="$SOURCES nostr_core/nsigner_transport.c"
|
||||
SOURCES="$SOURCES nostr_core/nsigner_client.c"
|
||||
CFLAGS="$CFLAGS -DNOSTR_ENABLE_NSIGNER_CLIENT=1"
|
||||
```
|
||||
|
||||
### `CMakeLists.txt`
|
||||
|
||||
Desktop branch (`else()`):
|
||||
|
||||
```cmake
|
||||
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/nostr_signer.c
|
||||
nostr_core/nsigner_transport.c
|
||||
nostr_core/nsigner_client.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_compile_definitions(nostr_core PRIVATE NOSTR_ENABLE_NSIGNER_CLIENT=1)
|
||||
```
|
||||
|
||||
ESP32 branch (`if(ESP_PLATFORM)`):
|
||||
|
||||
- Does **not** include `nostr_core/nsigner_transport.c` or `nostr_core/nsigner_client.c` in `idf_component_register(SRCS ...)`.
|
||||
- Sets only:
|
||||
|
||||
```cmake
|
||||
target_compile_definitions(${COMPONENT_LIB} PRIVATE NOSTR_NO_FILESYSTEM=1)
|
||||
```
|
||||
|
||||
## 8) Testing pointers
|
||||
|
||||
Relevant tests:
|
||||
|
||||
- `tests/nsigner_client_test.c`
|
||||
- `tests/signer_modules_test.c`
|
||||
- `tests/nip01_test.c` (includes local-signer equivalence checks)
|
||||
|
||||
Build tests:
|
||||
|
||||
```bash
|
||||
./build.sh -t
|
||||
```
|
||||
|
||||
Run target binaries:
|
||||
|
||||
```bash
|
||||
./tests/nsigner_client_test
|
||||
./tests/signer_modules_test
|
||||
./tests/nip01_test
|
||||
```
|
||||
|
||||
## 9) Driver app signer modes (`examples/note_poster.c`)
|
||||
|
||||
Supported `--signer` values in the driver app:
|
||||
|
||||
- `local` (default)
|
||||
- `unix:<name>`
|
||||
- `serial:<path>`
|
||||
- `tcp:<host>:<port>`
|
||||
- `fds:<read_fd>:<write_fd>`
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
./examples/note_poster
|
||||
./examples/note_poster --signer unix:<name>
|
||||
./examples/note_poster --signer serial:/dev/ttyACM0
|
||||
./examples/note_poster --signer tcp:<host>:<port>
|
||||
./examples/note_poster --signer fds:<read_fd>:<write_fd>
|
||||
```
|
||||
|
||||
Local mode warning (from app behavior): this is test-key-only usage; do not use production keys.
|
||||
@@ -1,642 +0,0 @@
|
||||
/*
|
||||
* 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_with_signer(nostr_signer_t* signer,
|
||||
const char* operation,
|
||||
const char* sha256_hex,
|
||||
int expiration_seconds,
|
||||
time_t timestamp) {
|
||||
if (!signer || !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);
|
||||
}
|
||||
|
||||
time_t now = (timestamp > 0) ? timestamp : time(NULL);
|
||||
int exp = expiration_seconds > 0 ? expiration_seconds : 300;
|
||||
long until = (long)now + 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_with_signer(24242, "", tags, signer, now);
|
||||
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;
|
||||
}
|
||||
|
||||
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 now = (long)time(NULL);
|
||||
long until = now + 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_t)now);
|
||||
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_with_signer(const char* server_url,
|
||||
const unsigned char* data,
|
||||
size_t data_len,
|
||||
const char* content_type,
|
||||
nostr_signer_t* signer,
|
||||
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 (signer) {
|
||||
auth = blossom_create_auth_header_with_signer(signer, "upload", hash_hex, 300, 0);
|
||||
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(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) {
|
||||
nostr_signer_t* signer = NULL;
|
||||
int rc;
|
||||
|
||||
if (private_key) {
|
||||
signer = nostr_signer_local(private_key);
|
||||
}
|
||||
|
||||
rc = blossom_upload_with_signer(server_url, data, data_len, content_type,
|
||||
signer, sha256_hex, timeout_seconds, descriptor_out);
|
||||
nostr_signer_free(signer);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int blossom_upload_file_with_signer(const char* server_url,
|
||||
const char* file_path,
|
||||
const char* content_type,
|
||||
nostr_signer_t* signer,
|
||||
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_with_signer(server_url, buf, n, content_type, signer, NULL, timeout_seconds, descriptor_out);
|
||||
free(buf);
|
||||
return rc;
|
||||
}
|
||||
|
||||
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) {
|
||||
nostr_signer_t* signer = NULL;
|
||||
int rc;
|
||||
|
||||
if (private_key) {
|
||||
signer = nostr_signer_local(private_key);
|
||||
}
|
||||
|
||||
rc = blossom_upload_file_with_signer(server_url, file_path, content_type,
|
||||
signer, timeout_seconds, descriptor_out);
|
||||
nostr_signer_free(signer);
|
||||
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_with_signer(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
nostr_signer_t* signer,
|
||||
int timeout_seconds) {
|
||||
if (!server_url || !sha256_hex || !signer || !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_with_signer(signer, "delete", sha256_hex, 300, 0);
|
||||
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_delete(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
const unsigned char* private_key,
|
||||
int timeout_seconds) {
|
||||
nostr_signer_t* signer;
|
||||
int rc;
|
||||
|
||||
if (!private_key) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
rc = blossom_delete_with_signer(server_url, sha256_hex, signer, timeout_seconds);
|
||||
nostr_signer_free(signer);
|
||||
return rc;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Blossom HTTP Client
|
||||
*/
|
||||
|
||||
#ifndef NOSTR_BLOSSOM_CLIENT_H
|
||||
#define NOSTR_BLOSSOM_CLIENT_H
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_signer.h"
|
||||
#include <stddef.h>
|
||||
#include <time.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);
|
||||
|
||||
char* blossom_create_auth_header_with_signer(nostr_signer_t* signer,
|
||||
const char* operation,
|
||||
const char* sha256_hex,
|
||||
int expiration_seconds,
|
||||
time_t timestamp);
|
||||
|
||||
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_with_signer(const char* server_url,
|
||||
const unsigned char* data,
|
||||
size_t data_len,
|
||||
const char* content_type,
|
||||
nostr_signer_t* signer,
|
||||
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_upload_file_with_signer(const char* server_url,
|
||||
const char* file_path,
|
||||
const char* content_type,
|
||||
nostr_signer_t* signer,
|
||||
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_delete_with_signer(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
nostr_signer_t* signer,
|
||||
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 */
|
||||
+108
-1228
File diff suppressed because it is too large
Load Diff
+1
-63
@@ -17,28 +17,11 @@ extern "C" {
|
||||
#define CASHU_API_VERSION "v1"
|
||||
|
||||
typedef struct {
|
||||
char id[NOSTR_CASHU_KEYSET_ID_HEX_SIZE];
|
||||
char id[17];
|
||||
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;
|
||||
@@ -123,38 +106,12 @@ 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,
|
||||
@@ -246,25 +203,6 @@ 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
|
||||
|
||||
+65
-249
@@ -178,9 +178,6 @@ 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 {
|
||||
@@ -208,8 +205,8 @@ struct nostr_relay_pool {
|
||||
|
||||
// NIP-42 Authentication configuration
|
||||
int nip42_enabled;
|
||||
nostr_signer_t* auth_signer;
|
||||
int owns_auth_signer;
|
||||
unsigned char private_key[32];
|
||||
int has_private_key;
|
||||
};
|
||||
|
||||
// Helper function to generate unique subscription IDs
|
||||
@@ -510,32 +507,16 @@ static void attempt_reconnect(relay_connection_t* relay) {
|
||||
relay->reconnect_attempts++;
|
||||
|
||||
if (ensure_relay_connection(relay) == 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;
|
||||
}
|
||||
// Success! Reset reconnection state
|
||||
relay->reconnect_attempts = 0;
|
||||
relay->next_reconnect_time = 0;
|
||||
|
||||
// Restore subscriptions on reconnect
|
||||
restore_subscriptions_on_reconnect(relay);
|
||||
} else {
|
||||
// Failed - schedule next attempt with backoff
|
||||
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;
|
||||
relay->next_reconnect_time = time(NULL) + (delay_ms / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,7 +615,6 @@ 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
|
||||
};
|
||||
@@ -664,20 +644,22 @@ nostr_relay_pool_t* nostr_relay_pool_create_compat(void) {
|
||||
return nostr_relay_pool_create(NULL);
|
||||
}
|
||||
|
||||
int nostr_relay_pool_set_auth_with_signer(nostr_relay_pool_t* pool,
|
||||
nostr_signer_t* signer,
|
||||
int enable) {
|
||||
int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool,
|
||||
const unsigned char* private_key,
|
||||
int enable) {
|
||||
if (!pool) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (pool->owns_auth_signer && pool->auth_signer) {
|
||||
nostr_signer_free(pool->auth_signer);
|
||||
}
|
||||
|
||||
pool->nip42_enabled = enable ? 1 : 0;
|
||||
pool->auth_signer = pool->nip42_enabled ? signer : NULL;
|
||||
pool->owns_auth_signer = 0;
|
||||
|
||||
if (private_key) {
|
||||
memcpy(pool->private_key, private_key, sizeof(pool->private_key));
|
||||
pool->has_private_key = 1;
|
||||
} else {
|
||||
memset(pool->private_key, 0, sizeof(pool->private_key));
|
||||
pool->has_private_key = 0;
|
||||
}
|
||||
|
||||
// Propagate to existing relay connections
|
||||
for (int i = 0; i < pool->relay_count; i++) {
|
||||
@@ -694,36 +676,6 @@ int nostr_relay_pool_set_auth_with_signer(nostr_relay_pool_t* pool,
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool,
|
||||
const unsigned char* private_key,
|
||||
int enable) {
|
||||
nostr_signer_t* signer = NULL;
|
||||
int rc;
|
||||
|
||||
if (!pool) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (enable && private_key) {
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
rc = nostr_relay_pool_set_auth_with_signer(pool, signer, enable);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
nostr_signer_free(signer);
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (signer) {
|
||||
pool->owns_auth_signer = 1;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url) {
|
||||
if (!pool || !relay_url || pool->relay_count >= NOSTR_POOL_MAX_RELAYS) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
@@ -844,10 +796,9 @@ void nostr_relay_pool_destroy(nostr_relay_pool_t* pool) {
|
||||
}
|
||||
}
|
||||
|
||||
if (pool->owns_auth_signer && pool->auth_signer) {
|
||||
nostr_signer_free(pool->auth_signer);
|
||||
pool->auth_signer = NULL;
|
||||
pool->owns_auth_signer = 0;
|
||||
if (pool->has_private_key) {
|
||||
memset(pool->private_key, 0, sizeof(pool->private_key));
|
||||
pool->has_private_key = 0;
|
||||
}
|
||||
|
||||
free(pool);
|
||||
@@ -1006,9 +957,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]);
|
||||
@@ -1016,7 +967,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++) {
|
||||
@@ -1029,7 +980,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++) {
|
||||
@@ -1037,55 +988,11 @@ 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) {
|
||||
@@ -1125,15 +1032,6 @@ 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) {
|
||||
@@ -1201,15 +1099,32 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
|
||||
}
|
||||
}
|
||||
|
||||
// Keep activity updated on explicit EOSE from this relay
|
||||
// Check if all relays have sent EOSE
|
||||
int all_eose = 1;
|
||||
for (int j = 0; j < sub->relay_count; j++) {
|
||||
if (strcmp(sub->relay_urls[j], relay->url) == 0) {
|
||||
sub->relay_last_activity[j] = time(NULL);
|
||||
if (!sub->eose_received[j]) {
|
||||
all_eose = 0;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
(void)maybe_complete_subscription_eose(sub);
|
||||
// Auto-close subscription if close_on_eose is enabled
|
||||
if (sub->close_on_eose && !sub->closed) {
|
||||
nostr_pool_subscription_close(sub);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1247,11 +1162,11 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
|
||||
relay->auth_state = NOSTR_AUTH_STATE_REJECTED;
|
||||
|
||||
// If we already have a challenge, immediately attempt re-authentication
|
||||
if (relay->nip42_enabled && pool->auth_signer && relay->auth_challenge[0] != '\0') {
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event_with_signer(
|
||||
if (relay->nip42_enabled && pool->has_private_key && relay->auth_challenge[0] != '\0') {
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(
|
||||
relay->auth_challenge,
|
||||
relay->url,
|
||||
pool->auth_signer,
|
||||
pool->private_key,
|
||||
0
|
||||
);
|
||||
if (auth_event) {
|
||||
@@ -1294,7 +1209,7 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
|
||||
}
|
||||
} else if (strcmp(msg_type, "AUTH") == 0) {
|
||||
// Handle AUTH challenge message: ["AUTH", <challenge-string>]
|
||||
if (relay->nip42_enabled && pool->auth_signer &&
|
||||
if (relay->nip42_enabled && pool->has_private_key &&
|
||||
cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
|
||||
cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1);
|
||||
if (cJSON_IsString(challenge_json)) {
|
||||
@@ -1305,10 +1220,10 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
|
||||
relay->auth_challenge_time = time(NULL);
|
||||
relay->auth_state = NOSTR_AUTH_STATE_CHALLENGE_RECEIVED;
|
||||
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event_with_signer(
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(
|
||||
challenge,
|
||||
relay->url,
|
||||
pool->auth_signer,
|
||||
pool->private_key,
|
||||
0
|
||||
);
|
||||
if (auth_event) {
|
||||
@@ -1378,72 +1293,28 @@ cJSON** nostr_relay_pool_query_sync(
|
||||
int connected_count = 0;
|
||||
|
||||
for (int i = 0; i < relay_count && connected_count < NOSTR_POOL_MAX_RELAYS; i++) {
|
||||
relay_connection_t* relay = NULL;
|
||||
int ensure_rc = -1;
|
||||
|
||||
query_sync_debug_log("connect_step_start idx=%d/%d relay=%s connected_so_far=%d",
|
||||
i + 1,
|
||||
relay_count,
|
||||
relay_urls[i] ? relay_urls[i] : "(null)",
|
||||
connected_count);
|
||||
|
||||
relay = find_relay_by_url(pool, relay_urls[i]);
|
||||
query_sync_debug_log("find_relay idx=%d relay=%s found=%s",
|
||||
i + 1,
|
||||
relay_urls[i] ? relay_urls[i] : "(null)",
|
||||
relay ? "yes" : "no");
|
||||
|
||||
relay_connection_t* relay = find_relay_by_url(pool, relay_urls[i]);
|
||||
if (!relay) {
|
||||
int add_rc = nostr_relay_pool_add_relay(pool, relay_urls[i]);
|
||||
query_sync_debug_log("add_relay idx=%d relay=%s rc=%d",
|
||||
i + 1,
|
||||
relay_urls[i] ? relay_urls[i] : "(null)",
|
||||
add_rc);
|
||||
if (add_rc == NOSTR_SUCCESS) {
|
||||
// Add relay if it doesn't exist
|
||||
if (nostr_relay_pool_add_relay(pool, relay_urls[i]) == NOSTR_SUCCESS) {
|
||||
relay = find_relay_by_url(pool, relay_urls[i]);
|
||||
query_sync_debug_log("find_relay_after_add idx=%d relay=%s found=%s",
|
||||
i + 1,
|
||||
relay_urls[i] ? relay_urls[i] : "(null)",
|
||||
relay ? "yes" : "no");
|
||||
}
|
||||
}
|
||||
|
||||
if (!relay) {
|
||||
query_sync_debug_log("connect_step_skip idx=%d relay=%s reason=no_relay_object",
|
||||
i + 1,
|
||||
relay_urls[i] ? relay_urls[i] : "(null)");
|
||||
continue;
|
||||
}
|
||||
|
||||
query_sync_debug_log("ensure_connection_begin idx=%d relay=%s", i + 1, relay->url);
|
||||
ensure_rc = ensure_relay_connection(relay);
|
||||
query_sync_debug_log("ensure_connection_end idx=%d relay=%s rc=%d ws_client=%s state=%d",
|
||||
i + 1,
|
||||
relay->url,
|
||||
ensure_rc,
|
||||
relay->ws_client ? "set" : "null",
|
||||
(int)relay->status);
|
||||
|
||||
if (ensure_rc == 0) {
|
||||
|
||||
if (relay && ensure_relay_connection(relay) == 0) {
|
||||
connected_relays[connected_count++] = relay;
|
||||
|
||||
|
||||
// Add subscription timing for latency measurement
|
||||
add_subscription_timing(relay, subscription_id);
|
||||
|
||||
|
||||
// Send REQ message
|
||||
int send_rc = nostr_relay_send_req(relay->ws_client, subscription_id, filter);
|
||||
query_sync_debug_log("send_req relay=%s sub_id=%s rc=%d connected_count=%d", relay->url, subscription_id, send_rc, connected_count);
|
||||
query_sync_debug_log("send_req relay=%s sub_id=%s rc=%d", relay->url, subscription_id, send_rc);
|
||||
if (send_rc < 0) {
|
||||
// Remove timing if send failed
|
||||
remove_subscription_timing(relay, subscription_id);
|
||||
connected_count--; // Don't count failed connections
|
||||
query_sync_debug_log("send_req_failed relay=%s sub_id=%s connected_count=%d", relay->url, subscription_id, connected_count);
|
||||
}
|
||||
} else {
|
||||
query_sync_debug_log("connect_step_skip idx=%d relay=%s reason=ensure_failed rc=%d",
|
||||
i + 1,
|
||||
relay->url,
|
||||
ensure_rc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1461,17 +1332,11 @@ cJSON** nostr_relay_pool_query_sync(
|
||||
query_sync_debug_log("query_loop_start sub_id=%s timeout_ms=%d timeout_seconds=%d connected_relays=%d", subscription_id, timeout_ms, timeout_seconds, connected_count);
|
||||
|
||||
while (time(NULL) - start_time < timeout_seconds && eose_count < connected_count) {
|
||||
query_sync_debug_log("query_loop_tick sub_id=%s elapsed_s=%d timeout_s=%d eose=%d/%d",
|
||||
subscription_id,
|
||||
(int)(time(NULL) - start_time),
|
||||
timeout_seconds,
|
||||
eose_count,
|
||||
connected_count);
|
||||
for (int i = 0; i < connected_count; i++) {
|
||||
relay_connection_t* relay = connected_relays[i];
|
||||
|
||||
char buffer[262144];
|
||||
int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, 1000);
|
||||
char buffer[65536];
|
||||
int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, 100);
|
||||
query_sync_debug_log("receive relay=%s len=%d", relay->url, len);
|
||||
if (len > 0) {
|
||||
buffer[len] = '\0';
|
||||
@@ -1565,7 +1430,7 @@ cJSON** nostr_relay_pool_query_sync(
|
||||
}
|
||||
}
|
||||
} else if (msg_type && strcmp(msg_type, "AUTH") == 0) {
|
||||
if (relay->nip42_enabled && pool->auth_signer &&
|
||||
if (relay->nip42_enabled && pool->has_private_key &&
|
||||
cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
|
||||
cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1);
|
||||
if (cJSON_IsString(challenge_json)) {
|
||||
@@ -1577,10 +1442,10 @@ cJSON** nostr_relay_pool_query_sync(
|
||||
relay->auth_challenge_time = time(NULL);
|
||||
relay->auth_state = NOSTR_AUTH_STATE_CHALLENGE_RECEIVED;
|
||||
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event_with_signer(
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(
|
||||
challenge,
|
||||
relay->url,
|
||||
pool->auth_signer,
|
||||
pool->private_key,
|
||||
0
|
||||
);
|
||||
if (auth_event) {
|
||||
@@ -2129,21 +1994,6 @@ 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);
|
||||
|
||||
@@ -2168,39 +2018,5 @@ 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;
|
||||
}
|
||||
|
||||
@@ -195,10 +195,9 @@ cJSON** synchronous_query_relays_with_progress(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to receive message (1000ms timeout to allow relay time
|
||||
// to respond — matches nostr_relay_pool_query_sync behavior)
|
||||
char buffer[262144];
|
||||
int len = nostr_ws_receive(relay->client, buffer, sizeof(buffer)-1, 1000);
|
||||
// Try to receive message (short timeout to keep UI responsive)
|
||||
char buffer[8192];
|
||||
int len = nostr_ws_receive(relay->client, buffer, sizeof(buffer)-1, 50);
|
||||
|
||||
if (len > 0) {
|
||||
buffer[len] = '\0';
|
||||
@@ -323,23 +322,6 @@ cJSON** synchronous_query_relays_with_progress(
|
||||
nostr_ws_close(relay->client);
|
||||
relay->client = NULL;
|
||||
}
|
||||
} else if (msg_type && (strcmp(msg_type, "NOTICE") == 0 ||
|
||||
strcmp(msg_type, "CLOSED") == 0)) {
|
||||
/* Capture NOTICE and CLOSED messages from the relay.
|
||||
* These often contain auth-required or restriction
|
||||
* notices. Pass the message content to the callback. */
|
||||
const char* notice_msg = "";
|
||||
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
|
||||
cJSON* msg_json = cJSON_GetArrayItem(parsed, 1);
|
||||
if (msg_json && cJSON_IsString(msg_json)) {
|
||||
notice_msg = cJSON_GetStringValue(msg_json);
|
||||
}
|
||||
}
|
||||
if (callback) {
|
||||
callback(relay->url, msg_type, notice_msg,
|
||||
relay->events_received, relay_count,
|
||||
completed_relays, user_data);
|
||||
}
|
||||
}
|
||||
|
||||
if (msg_type) free(msg_type);
|
||||
@@ -598,7 +580,7 @@ publish_result_t* synchronous_publish_event_with_progress(
|
||||
}
|
||||
|
||||
// Try to receive message (short timeout to keep UI responsive)
|
||||
char buffer[262144];
|
||||
char buffer[8192];
|
||||
int len = nostr_ws_receive(relay->client, buffer, sizeof(buffer)-1, 50);
|
||||
|
||||
if (len > 0) {
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -3,8 +3,9 @@
|
||||
#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
|
||||
@@ -236,85 +237,28 @@ 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;
|
||||
}
|
||||
|
||||
if (nostr_platform_random(buf, 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
+81
-39
@@ -7,7 +7,6 @@
|
||||
#include "nip001.h"
|
||||
#include "utils.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
#include "nostr_signer.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -33,73 +32,116 @@ int nostr_ec_sign(const unsigned char* private_key, const unsigned char* hash, u
|
||||
/**
|
||||
* Create and sign a NOSTR event
|
||||
*/
|
||||
cJSON* nostr_create_and_sign_event_with_signer(int kind, const char* content, cJSON* tags, nostr_signer_t* signer, time_t timestamp) {
|
||||
cJSON* event = NULL;
|
||||
cJSON* signed_event = NULL;
|
||||
char pubkey_hex[65];
|
||||
int rc;
|
||||
|
||||
if (!signer) {
|
||||
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp) {
|
||||
if (!private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
if (!content) {
|
||||
content = ""; // Default to empty content
|
||||
}
|
||||
|
||||
rc = nostr_signer_get_public_key(signer, pubkey_hex);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
|
||||
// Convert private key to public key
|
||||
unsigned char public_key[32];
|
||||
if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// Convert public key to hex
|
||||
char pubkey_hex[65];
|
||||
nostr_bytes_to_hex(public_key, 32, pubkey_hex);
|
||||
|
||||
// Create event structure
|
||||
event = cJSON_CreateObject();
|
||||
cJSON* event = cJSON_CreateObject();
|
||||
if (!event) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// Use provided timestamp or current time if timestamp is 0
|
||||
time_t event_time = (timestamp == 0) ? time(NULL) : timestamp;
|
||||
|
||||
|
||||
cJSON_AddStringToObject(event, "pubkey", pubkey_hex);
|
||||
cJSON_AddNumberToObject(event, "created_at", (double)event_time);
|
||||
cJSON_AddNumberToObject(event, "kind", kind);
|
||||
|
||||
|
||||
// Add tags (copy provided tags or create empty array)
|
||||
if (tags) {
|
||||
cJSON_AddItemToObject(event, "tags", cJSON_Duplicate(tags, 1));
|
||||
} else {
|
||||
cJSON_AddItemToObject(event, "tags", cJSON_CreateArray());
|
||||
}
|
||||
|
||||
|
||||
cJSON_AddStringToObject(event, "content", content);
|
||||
|
||||
rc = nostr_signer_sign_event(signer, event, &signed_event);
|
||||
cJSON_Delete(event);
|
||||
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
|
||||
// ============================================================================
|
||||
// INLINE SERIALIZATION AND SIGNING LOGIC
|
||||
// ============================================================================
|
||||
|
||||
// Get event fields for serialization
|
||||
cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey");
|
||||
cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at");
|
||||
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
|
||||
cJSON* tags_item = cJSON_GetObjectItem(event, "tags");
|
||||
cJSON* content_item = cJSON_GetObjectItem(event, "content");
|
||||
|
||||
if (!pubkey_item || !created_at_item || !kind_item || !tags_item || !content_item) {
|
||||
cJSON_Delete(event);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return signed_event;
|
||||
}
|
||||
|
||||
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp) {
|
||||
cJSON* out = NULL;
|
||||
nostr_signer_t* signer = NULL;
|
||||
|
||||
if (!private_key) {
|
||||
|
||||
// Create serialization array: [0, pubkey, created_at, kind, tags, content]
|
||||
cJSON* serialize_array = cJSON_CreateArray();
|
||||
if (!serialize_array) {
|
||||
cJSON_Delete(event);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
|
||||
cJSON_AddItemToArray(serialize_array, cJSON_CreateNumber(0));
|
||||
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(pubkey_item, 1));
|
||||
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(created_at_item, 1));
|
||||
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(kind_item, 1));
|
||||
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(tags_item, 1));
|
||||
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(content_item, 1));
|
||||
|
||||
char* serialize_string = cJSON_PrintUnformatted(serialize_array);
|
||||
cJSON_Delete(serialize_array);
|
||||
|
||||
if (!serialize_string) {
|
||||
cJSON_Delete(event);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out = nostr_create_and_sign_event_with_signer(kind, content, tags, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return out;
|
||||
|
||||
// Hash the serialized event
|
||||
unsigned char event_hash[32];
|
||||
if (nostr_sha256((const unsigned char*)serialize_string, strlen(serialize_string), event_hash) != 0) {
|
||||
free(serialize_string);
|
||||
cJSON_Delete(event);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Convert hash to hex for event ID
|
||||
char event_id[65];
|
||||
nostr_bytes_to_hex(event_hash, 32, event_id);
|
||||
|
||||
// Sign the hash using ECDSA
|
||||
unsigned char signature[64];
|
||||
if (nostr_ec_sign(private_key, event_hash, signature) != 0) {
|
||||
free(serialize_string);
|
||||
cJSON_Delete(event);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Convert signature to hex
|
||||
char sig_hex[129];
|
||||
nostr_bytes_to_hex(signature, 64, sig_hex);
|
||||
|
||||
// Add ID and signature to the event
|
||||
cJSON_AddStringToObject(event, "id", event_id);
|
||||
cJSON_AddStringToObject(event, "sig", sig_hex);
|
||||
|
||||
free(serialize_string);
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,12 +10,10 @@
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include "../cjson/cJSON.h"
|
||||
#include "nostr_signer.h"
|
||||
|
||||
|
||||
// Function declarations
|
||||
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp);
|
||||
cJSON* nostr_create_and_sign_event_with_signer(int kind, const char* content, cJSON* tags, nostr_signer_t* signer, time_t timestamp);
|
||||
|
||||
// Event validation functions
|
||||
int nostr_validate_event_structure(cJSON* event);
|
||||
|
||||
-1513
File diff suppressed because it is too large
Load Diff
@@ -1,257 +0,0 @@
|
||||
/*
|
||||
* NOSTR Core Library - NIP-03: OpenTimestamps Attestations for Events
|
||||
*/
|
||||
|
||||
#ifndef NOSTR_NIP03_H
|
||||
#define NOSTR_NIP03_H
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_signer.h"
|
||||
#include "cjson/cJSON.h"
|
||||
#include <time.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define NOSTR_KIND_OT_PROOF 1040
|
||||
|
||||
/* Default OTS calendars for multi-calendar stamping */
|
||||
#define NOSTR_OTS_DEFAULT_CALENDAR_COUNT 4
|
||||
extern const char* NOSTR_OTS_DEFAULT_CALENDARS[NOSTR_OTS_DEFAULT_CALENDAR_COUNT];
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Create a NIP-03 OpenTimestamps proof event using a signer (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 signer Signer handle for signing the proof event
|
||||
* @return cJSON* The signed proof event, or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip03_create_proof_event_with_signer(const char* target_event_id,
|
||||
int target_event_kind,
|
||||
const char* ots_data_base64,
|
||||
const char* relay_url,
|
||||
nostr_signer_t* signer);
|
||||
|
||||
/**
|
||||
* 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 or Litecoin attestation)
|
||||
*
|
||||
* Parses the OTS binary file format and searches for a Bitcoin block header
|
||||
* attestation (tag 0588960d73d71901) or Litecoin attestation (tag 06856e0d73d71901).
|
||||
* A proof with only pending attestations is not yet complete.
|
||||
*
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @return int 1 if complete (has Bitcoin/Litecoin attestation), 0 if pending, -1 on error
|
||||
*/
|
||||
int nostr_nip03_is_proof_complete(const char* ots_data_base64);
|
||||
|
||||
/**
|
||||
* Get detailed attestation information from an OTS proof
|
||||
*
|
||||
* Parses the OTS file and extracts information about all attestations found.
|
||||
*
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @param has_bitcoin Output: 1 if a Bitcoin attestation is present (optional, NULL to skip)
|
||||
* @param has_litecoin Output: 1 if a Litecoin attestation is present (optional, NULL to skip)
|
||||
* @param has_pending Output: 1 if any pending attestations are present (optional, NULL to skip)
|
||||
* @param bitcoin_height Output: Bitcoin block height (optional, NULL to skip)
|
||||
* @param litecoin_height Output: Litecoin block height (optional, NULL to skip)
|
||||
* @param pending_uri_out Output: First pending calendar URI (optional, NULL to skip, caller must free)
|
||||
* @return int 0 on success, -1 on parse error
|
||||
*/
|
||||
int nostr_nip03_get_attestation_info(const char* ots_data_base64,
|
||||
int* has_bitcoin,
|
||||
int* has_litecoin,
|
||||
int* has_pending,
|
||||
uint64_t* bitcoin_height,
|
||||
uint64_t* litecoin_height,
|
||||
char** pending_uri_out);
|
||||
|
||||
/**
|
||||
* Upgrade an OTS proof by fetching missing attestations from a calendar
|
||||
*
|
||||
* Sends the current proof to the calendar's /upgrade endpoint and returns
|
||||
* the updated proof. The calendar may return the same proof if no new
|
||||
* attestations are available, or an upgraded proof with Bitcoin/Litecoin
|
||||
* attestations.
|
||||
*
|
||||
* @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 (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_upgrade_proof(const char* ots_data_base64,
|
||||
const char* calendar_url,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Upgrade an OTS proof by re-submitting the digest to a calendar
|
||||
*
|
||||
* This is the preferred upgrade method for raw timestamp proofs (returned by
|
||||
* the calendar /digest endpoint) which don't contain the file digest.
|
||||
* Re-submits the digest to the calendar, which returns the current proof
|
||||
* that may include new Bitcoin attestations.
|
||||
*
|
||||
* @param digest_hex The 64-char hex SHA-256 digest of the timestamped data
|
||||
* @param calendar_url The OTS calendar URL
|
||||
* @param timeout_seconds Request timeout
|
||||
* @return char* New base64 encoded .ots data, or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_upgrade_proof_with_digest(const char* digest_hex,
|
||||
const char* calendar_url,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Upgrade an OTS proof by walking the proof tree and fetching upgraded sub-proofs
|
||||
*
|
||||
* This is the correct upgrade mechanism, matching the reference ots client:
|
||||
* 1. Parses the proof tree
|
||||
* 2. Walks the tree computing intermediate commitment hashes
|
||||
* 3. For each PendingAttestation, calls GET /timestamp/<commitment-hash>
|
||||
* 4. If the calendar returns a proof with a Bitcoin attestation, splices it in
|
||||
*
|
||||
* @param ots_data_base64 Base64 encoded DetachedTimestampFile
|
||||
* @param timeout_seconds Per-request timeout
|
||||
* @return char* New base64 encoded upgraded proof, or NULL if no upgrade available (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_upgrade_proof_tree(const char* ots_data_base64,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Get all pending calendar URIs from an OTS proof
|
||||
*
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @param uris_out Array of char* pointers (caller must free each string and the array)
|
||||
* @param max_uris Maximum number of URIs to return
|
||||
* @return int Number of pending URIs found, or -1 on error
|
||||
*/
|
||||
int nostr_nip03_get_pending_uris(const char* ots_data_base64,
|
||||
char*** uris_out,
|
||||
int max_uris);
|
||||
|
||||
/**
|
||||
* Create a full DetachedTimestampFile (.ots) from a digest
|
||||
*
|
||||
* Wraps a SHA-256 digest in the OTS DetachedTimestampFile format with the
|
||||
* proper header magic, version, hash op, and digest. This is the standard
|
||||
* .ots file format that can be verified by the `ots` command-line tool.
|
||||
*
|
||||
* @param digest_hex The 64-char hex SHA-256 digest
|
||||
* @return char* Base64 encoded DetachedTimestampFile, or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_create_ots_file(const char* digest_hex);
|
||||
|
||||
/**
|
||||
* Submit a digest to multiple calendars and create a combined proof
|
||||
*
|
||||
* Submits the digest to each calendar, collects all the returned timestamps,
|
||||
* and combines them into a single DetachedTimestampFile with a fork operation.
|
||||
* This is the recommended way to timestamp data — using multiple calendars
|
||||
* provides redundancy.
|
||||
*
|
||||
* @param digest_hex The 64-char hex SHA-256 digest
|
||||
* @param calendar_urls Array of calendar URLs
|
||||
* @param calendar_count Number of calendars
|
||||
* @param timeout_seconds Per-request timeout
|
||||
* @return char* Base64 encoded DetachedTimestampFile with all calendar proofs, or NULL on error
|
||||
*/
|
||||
char* nostr_nip03_stamp_with_multiple_calendars(const char* digest_hex,
|
||||
const char** calendar_urls,
|
||||
int calendar_count,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Request a timestamp for an event ID from multiple OTS calendars
|
||||
*
|
||||
* Submits the event ID to multiple calendars and combines the results into a
|
||||
* single DetachedTimestampFile. Uses the default calendars if none specified.
|
||||
*
|
||||
* @param event_id_hex The event ID to timestamp (64-char hex string)
|
||||
* @param calendar_urls Array of calendar URLs (NULL to use defaults)
|
||||
* @param calendar_count Number of calendars (0 to use defaults)
|
||||
* @param timeout_seconds Per-request timeout
|
||||
* @return char* Base64 encoded DetachedTimestampFile, or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_request_timestamp_multi(const char* event_id_hex,
|
||||
const char** calendar_urls,
|
||||
int calendar_count,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Upgrade an OTS proof by re-submitting to multiple calendars
|
||||
*
|
||||
* Re-submits the digest to all specified calendars (or defaults) and returns
|
||||
* a new combined proof. The proof may now contain Bitcoin attestations if
|
||||
* any calendar has committed since the original submission.
|
||||
*
|
||||
* @param digest_hex The 64-char hex SHA-256 digest
|
||||
* @param calendar_urls Array of calendar URLs (NULL to use defaults)
|
||||
* @param calendar_count Number of calendars (0 to use defaults)
|
||||
* @param timeout_seconds Per-request timeout
|
||||
* @return char* New base64 encoded DetachedTimestampFile, or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_upgrade_proof_multi(const char* digest_hex,
|
||||
const char** calendar_urls,
|
||||
int calendar_count,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Verify an OTS proof structurally
|
||||
*
|
||||
* Parses the OTS file, optionally verifies the file digest matches the
|
||||
* expected target, and checks for a Bitcoin block header attestation.
|
||||
*
|
||||
* Note: Full on-chain verification (checking the Bitcoin block header's
|
||||
* merkle root) requires fetching the block header from a blockchain API.
|
||||
* This function performs structural verification only.
|
||||
*
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @param target_digest_hex Expected SHA256 digest of the timestamped data (64-char hex, or NULL to skip)
|
||||
* @param block_height_out Output: Bitcoin block height (optional, NULL to skip)
|
||||
* @param block_time_out Output: Block time (optional, NULL to skip, always 0 for structural verification)
|
||||
* @return int 0 on success (verified), 1 if no Bitcoin attestation found, -1 on parse error, -2 on digest mismatch
|
||||
*/
|
||||
int nostr_nip03_verify_proof(const char* ots_data_base64,
|
||||
const char* target_digest_hex,
|
||||
uint64_t* block_height_out,
|
||||
time_t* block_time_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_NIP03_H */
|
||||
+79
-13
@@ -12,7 +12,46 @@
|
||||
#include <ctype.h>
|
||||
|
||||
|
||||
#include "nostr_http.h"
|
||||
#include <curl/curl.h>
|
||||
|
||||
|
||||
// Structure for HTTP response handling
|
||||
typedef struct {
|
||||
char* data;
|
||||
size_t size;
|
||||
size_t capacity;
|
||||
} nip05_http_response_t;
|
||||
|
||||
/**
|
||||
* Callback function for curl to write HTTP response data
|
||||
*/
|
||||
static size_t nip05_write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
nip05_http_response_t* response = (nip05_http_response_t*)userp;
|
||||
size_t total_size = size * nmemb;
|
||||
|
||||
// Check if we need to expand the buffer
|
||||
if (response->size + total_size >= response->capacity) {
|
||||
size_t new_capacity = response->capacity * 2;
|
||||
if (new_capacity < response->size + total_size + 1) {
|
||||
new_capacity = response->size + total_size + 1;
|
||||
}
|
||||
|
||||
char* new_data = realloc(response->data, new_capacity);
|
||||
if (!new_data) {
|
||||
return 0; // Out of memory
|
||||
}
|
||||
|
||||
response->data = new_data;
|
||||
response->capacity = new_capacity;
|
||||
}
|
||||
|
||||
// Append the new data
|
||||
memcpy(response->data + response->size, contents, total_size);
|
||||
response->size += total_size;
|
||||
response->data[response->size] = '\0';
|
||||
|
||||
return total_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate a NIP-05 identifier into local part and domain
|
||||
@@ -65,22 +104,49 @@ static int nip05_http_get(const char* url, int timeout_seconds, char** response_
|
||||
if (!url || !response_data) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
long status = 0;
|
||||
int rc = nostr_http_get(url,
|
||||
timeout_seconds > 0 ? timeout_seconds : NIP05_DEFAULT_TIMEOUT,
|
||||
response_data,
|
||||
&status);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
nip05_http_response_t response = {0};
|
||||
response.capacity = 1024;
|
||||
response.data = malloc(response.capacity);
|
||||
if (!response.data) {
|
||||
curl_easy_cleanup(curl);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
response.data[0] = '\0';
|
||||
|
||||
// Set curl options with proper type casting to fix warnings
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)nip05_write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)(timeout_seconds > 0 ? timeout_seconds : NIP05_DEFAULT_TIMEOUT));
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); // NIP-05 forbids redirects
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "nostr-core/1.0");
|
||||
|
||||
// Perform the request
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
long response_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
free(response.data);
|
||||
return NOSTR_ERROR_NIP05_HTTP_FAILED;
|
||||
}
|
||||
|
||||
if (status != 200) {
|
||||
free(*response_data);
|
||||
*response_data = NULL;
|
||||
|
||||
if (response_code != 200) {
|
||||
free(response.data);
|
||||
return NOSTR_ERROR_NIP05_HTTP_FAILED;
|
||||
}
|
||||
|
||||
|
||||
*response_data = response.data;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
+17
-3
@@ -10,17 +10,24 @@
|
||||
#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;
|
||||
}
|
||||
|
||||
if (nostr_platform_random(private_key, 32) != 0) {
|
||||
// Generate random entropy using /dev/urandom
|
||||
FILE* urandom = fopen("/dev/urandom", "rb");
|
||||
if (!urandom) {
|
||||
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;
|
||||
@@ -43,10 +50,17 @@ int nostr_generate_mnemonic_and_keys(char* mnemonic, size_t mnemonic_size,
|
||||
|
||||
// Generate entropy for 12-word mnemonic
|
||||
unsigned char entropy[16];
|
||||
if (nostr_platform_random(entropy, sizeof(entropy)) != 0) {
|
||||
FILE* urandom = fopen("/dev/urandom", "rb");
|
||||
if (!urandom) {
|
||||
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;
|
||||
|
||||
+87
-25
@@ -11,13 +11,50 @@
|
||||
|
||||
#ifndef DISABLE_NIP05 // NIP-11 uses the same HTTP infrastructure as NIP-05
|
||||
|
||||
#include "nostr_http.h"
|
||||
#include <curl/curl.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
|
||||
*/
|
||||
@@ -304,35 +341,60 @@ int nostr_nip11_fetch_relay_info(const char* relay_url, nostr_relay_info_t** inf
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
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);
|
||||
// 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);
|
||||
free(http_url);
|
||||
|
||||
if (http_rc != NOSTR_SUCCESS || response.status_code != 200) {
|
||||
if (http_rc == NOSTR_SUCCESS) {
|
||||
nostr_http_response_free(&response);
|
||||
}
|
||||
|
||||
if (res != CURLE_OK || response_code != 200) {
|
||||
free(response.data);
|
||||
free(info);
|
||||
return NOSTR_ERROR_NIP05_HTTP_FAILED;
|
||||
}
|
||||
|
||||
|
||||
// Parse the relay information
|
||||
int parse_result = nip11_parse_relay_info(response.body, info);
|
||||
nostr_http_response_free(&response);
|
||||
int parse_result = nip11_parse_relay_info(response.data, info);
|
||||
free(response.data);
|
||||
|
||||
if (parse_result != NOSTR_SUCCESS) {
|
||||
nostr_nip11_relay_info_free(info);
|
||||
|
||||
+47
-115
@@ -216,34 +216,21 @@ cJSON* nostr_nip17_create_file_event(const char* file_url,
|
||||
cJSON* nostr_nip17_create_relay_list_event(const char** relay_urls,
|
||||
int num_relays,
|
||||
const unsigned char* private_key) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* out;
|
||||
|
||||
if (!private_key) {
|
||||
if (!relay_urls || num_relays <= 0 || !private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
// Get public key
|
||||
unsigned char public_key[32];
|
||||
if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out = nostr_nip17_create_relay_list_event_with_signer(relay_urls, num_relays, signer);
|
||||
nostr_signer_free(signer);
|
||||
return out;
|
||||
}
|
||||
char pubkey_hex[65];
|
||||
nostr_bytes_to_hex(public_key, 32, pubkey_hex);
|
||||
|
||||
cJSON* nostr_nip17_create_relay_list_event_with_signer(const char** relay_urls,
|
||||
int num_relays,
|
||||
nostr_signer_t* signer) {
|
||||
cJSON* tags;
|
||||
cJSON* relay_event;
|
||||
|
||||
if (!relay_urls || num_relays <= 0 || !signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
tags = cJSON_CreateArray();
|
||||
// Create tags with relay URLs
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -259,8 +246,11 @@ cJSON* nostr_nip17_create_relay_list_event_with_signer(const char** relay_urls,
|
||||
cJSON_AddItemToArray(tags, relay_tag);
|
||||
}
|
||||
|
||||
relay_event = nostr_create_and_sign_event_with_signer(10050, "", tags, signer, time(NULL));
|
||||
cJSON_Delete(tags);
|
||||
// Create and sign the event
|
||||
cJSON* relay_event = nostr_create_and_sign_event(10050, "", tags, private_key, time(NULL));
|
||||
|
||||
cJSON_Delete(tags); // Tags are duplicated in create_and_sign_event
|
||||
|
||||
return relay_event;
|
||||
}
|
||||
|
||||
@@ -274,65 +264,49 @@ int nostr_nip17_send_dm(cJSON* dm_event,
|
||||
cJSON** gift_wraps_out,
|
||||
int max_gift_wraps,
|
||||
long max_delay_sec) {
|
||||
nostr_signer_t* signer;
|
||||
int out;
|
||||
|
||||
if (!sender_private_key) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(sender_private_key);
|
||||
if (!signer) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out = nostr_nip17_send_dm_with_signer(dm_event, recipient_pubkeys, num_recipients, signer,
|
||||
gift_wraps_out, max_gift_wraps, max_delay_sec);
|
||||
nostr_signer_free(signer);
|
||||
return out;
|
||||
}
|
||||
|
||||
int nostr_nip17_send_dm_with_signer(cJSON* dm_event,
|
||||
const char** recipient_pubkeys,
|
||||
int num_recipients,
|
||||
nostr_signer_t* signer,
|
||||
cJSON** gift_wraps_out,
|
||||
int max_gift_wraps,
|
||||
long max_delay_sec) {
|
||||
int created_wraps = 0;
|
||||
char sender_pubkey_hex[65];
|
||||
|
||||
if (!dm_event || !recipient_pubkeys || num_recipients <= 0 ||
|
||||
!signer || !gift_wraps_out || max_gift_wraps <= 0) {
|
||||
!sender_private_key || !gift_wraps_out || max_gift_wraps <= 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int created_wraps = 0;
|
||||
|
||||
for (int i = 0; i < num_recipients && created_wraps < max_gift_wraps; i++) {
|
||||
// Convert recipient pubkey hex to bytes
|
||||
unsigned char recipient_public_key[32];
|
||||
if (nostr_hex_to_bytes(recipient_pubkeys[i], recipient_public_key, 32) != 0) {
|
||||
continue;
|
||||
continue; // Skip invalid pubkeys
|
||||
}
|
||||
|
||||
cJSON* seal = nostr_nip59_create_seal_with_signer(dm_event, signer, recipient_public_key, max_delay_sec);
|
||||
// Create seal for this recipient
|
||||
cJSON* seal = nostr_nip59_create_seal(dm_event, sender_private_key, recipient_public_key, max_delay_sec);
|
||||
if (!seal) {
|
||||
continue;
|
||||
continue; // Skip if sealing fails
|
||||
}
|
||||
|
||||
// Create gift wrap for this recipient
|
||||
cJSON* gift_wrap = nostr_nip59_create_gift_wrap(seal, recipient_pubkeys[i], max_delay_sec);
|
||||
cJSON_Delete(seal);
|
||||
cJSON_Delete(seal); // Seal is now wrapped
|
||||
|
||||
if (!gift_wrap) {
|
||||
continue;
|
||||
continue; // Skip if wrapping fails
|
||||
}
|
||||
|
||||
gift_wraps_out[created_wraps++] = gift_wrap;
|
||||
}
|
||||
|
||||
if (created_wraps < max_gift_wraps && nostr_signer_get_public_key(signer, sender_pubkey_hex) == NOSTR_SUCCESS) {
|
||||
// Also create a gift wrap for the sender (so they can see their own messages)
|
||||
if (created_wraps < max_gift_wraps) {
|
||||
// Get sender's public key
|
||||
unsigned char sender_public_key[32];
|
||||
if (nostr_hex_to_bytes(sender_pubkey_hex, sender_public_key, 32) == 0) {
|
||||
cJSON* sender_seal = nostr_nip59_create_seal_with_signer(dm_event, signer, sender_public_key, max_delay_sec);
|
||||
if (nostr_ec_public_key_from_private_key(sender_private_key, sender_public_key) == 0) {
|
||||
char sender_pubkey_hex[65];
|
||||
nostr_bytes_to_hex(sender_public_key, 32, sender_pubkey_hex);
|
||||
|
||||
// Create seal for sender
|
||||
cJSON* sender_seal = nostr_nip59_create_seal(dm_event, sender_private_key, sender_public_key, max_delay_sec);
|
||||
if (sender_seal) {
|
||||
// Create gift wrap for sender
|
||||
cJSON* sender_gift_wrap = nostr_nip59_create_gift_wrap(sender_seal, sender_pubkey_hex, max_delay_sec);
|
||||
cJSON_Delete(sender_seal);
|
||||
|
||||
@@ -351,77 +325,35 @@ int nostr_nip17_send_dm_with_signer(cJSON* dm_event,
|
||||
*/
|
||||
cJSON* nostr_nip17_receive_dm(cJSON* gift_wrap,
|
||||
const unsigned char* recipient_private_key) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* out;
|
||||
|
||||
if (!recipient_private_key) {
|
||||
if (!gift_wrap || !recipient_private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(recipient_private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out = nostr_nip17_receive_dm_with_signer(gift_wrap, signer);
|
||||
nostr_signer_free(signer);
|
||||
return out;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip17_receive_dm_with_signer(cJSON* gift_wrap,
|
||||
nostr_signer_t* signer) {
|
||||
cJSON* seal;
|
||||
cJSON* seal_pubkey_item;
|
||||
const char* sender_pubkey_hex;
|
||||
unsigned char sender_public_key[32];
|
||||
char sender_pubkey_hex_copy[65];
|
||||
cJSON* rumor;
|
||||
cJSON* rumor_pubkey_item;
|
||||
const char* rumor_pubkey_hex;
|
||||
|
||||
if (!gift_wrap || !signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
seal = nostr_nip59_unwrap_gift_with_signer(gift_wrap, signer);
|
||||
// Unwrap the gift wrap to get the seal
|
||||
cJSON* seal = nostr_nip59_unwrap_gift(gift_wrap, recipient_private_key);
|
||||
if (!seal) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
seal_pubkey_item = cJSON_GetObjectItem(seal, "pubkey");
|
||||
// Get sender's public key from the seal
|
||||
cJSON* seal_pubkey_item = cJSON_GetObjectItem(seal, "pubkey");
|
||||
if (!seal_pubkey_item || !cJSON_IsString(seal_pubkey_item)) {
|
||||
cJSON_Delete(seal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sender_pubkey_hex = cJSON_GetStringValue(seal_pubkey_item);
|
||||
if (!sender_pubkey_hex || strlen(sender_pubkey_hex) != 64) {
|
||||
cJSON_Delete(seal);
|
||||
return NULL;
|
||||
}
|
||||
memcpy(sender_pubkey_hex_copy, sender_pubkey_hex, 65);
|
||||
if (nostr_hex_to_bytes(sender_pubkey_hex_copy, sender_public_key, 32) != 0) {
|
||||
const char* sender_pubkey_hex = cJSON_GetStringValue(seal_pubkey_item);
|
||||
|
||||
// Convert sender pubkey hex to bytes
|
||||
unsigned char sender_public_key[32];
|
||||
if (nostr_hex_to_bytes(sender_pubkey_hex, sender_public_key, 32) != 0) {
|
||||
cJSON_Delete(seal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rumor = nostr_nip59_unseal_rumor_with_signer(seal, sender_public_key, signer);
|
||||
cJSON_Delete(seal);
|
||||
if (!rumor) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rumor_pubkey_item = cJSON_GetObjectItem(rumor, "pubkey");
|
||||
if (!rumor_pubkey_item || !cJSON_IsString(rumor_pubkey_item)) {
|
||||
cJSON_Delete(rumor);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rumor_pubkey_hex = cJSON_GetStringValue(rumor_pubkey_item);
|
||||
if (!rumor_pubkey_hex || strcmp(sender_pubkey_hex_copy, rumor_pubkey_hex) != 0) {
|
||||
cJSON_Delete(rumor);
|
||||
return NULL;
|
||||
}
|
||||
// Unseal the rumor
|
||||
cJSON* rumor = nostr_nip59_unseal_rumor(seal, sender_public_key, recipient_private_key);
|
||||
cJSON_Delete(seal); // Seal is no longer needed
|
||||
|
||||
return rumor;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#define NOSTR_NIP017_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include "nostr_signer.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
@@ -85,9 +84,6 @@ cJSON* nostr_nip17_create_file_event(const char* file_url,
|
||||
cJSON* nostr_nip17_create_relay_list_event(const char** relay_urls,
|
||||
int num_relays,
|
||||
const unsigned char* private_key);
|
||||
cJSON* nostr_nip17_create_relay_list_event_with_signer(const char** relay_urls,
|
||||
int num_relays,
|
||||
nostr_signer_t* signer);
|
||||
|
||||
/**
|
||||
* NIP-17: Send a direct message to recipients
|
||||
@@ -111,13 +107,6 @@ int nostr_nip17_send_dm(cJSON* dm_event,
|
||||
cJSON** gift_wraps_out,
|
||||
int max_gift_wraps,
|
||||
long max_delay_sec);
|
||||
int nostr_nip17_send_dm_with_signer(cJSON* dm_event,
|
||||
const char** recipient_pubkeys,
|
||||
int num_recipients,
|
||||
nostr_signer_t* signer,
|
||||
cJSON** gift_wraps_out,
|
||||
int max_gift_wraps,
|
||||
long max_delay_sec);
|
||||
|
||||
/**
|
||||
* NIP-17: Receive and decrypt a direct message
|
||||
@@ -130,8 +119,6 @@ int nostr_nip17_send_dm_with_signer(cJSON* dm_event,
|
||||
*/
|
||||
cJSON* nostr_nip17_receive_dm(cJSON* gift_wrap,
|
||||
const unsigned char* recipient_private_key);
|
||||
cJSON* nostr_nip17_receive_dm_with_signer(cJSON* gift_wrap,
|
||||
nostr_signer_t* signer);
|
||||
|
||||
/**
|
||||
* NIP-17: Extract DM relay URLs from a user's kind 10050 event
|
||||
|
||||
+1
-1
@@ -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((unsigned char)input[i])) {
|
||||
if (!isxdigit(input[i])) {
|
||||
is_hex = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
+11
-30
@@ -23,17 +23,17 @@ int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len);
|
||||
/**
|
||||
* Create NIP-42 authentication event (kind 22242)
|
||||
*/
|
||||
cJSON* nostr_nip42_create_auth_event_with_signer(const char* challenge,
|
||||
const char* relay_url,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!challenge || !relay_url || !signer) {
|
||||
cJSON* nostr_nip42_create_auth_event(const char* challenge,
|
||||
const char* relay_url,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
if (!challenge || !relay_url || !private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Validate challenge format
|
||||
size_t challenge_len = strlen(challenge);
|
||||
if (challenge_len < NOSTR_NIP42_MIN_CHALLENGE_LENGTH ||
|
||||
if (challenge_len < NOSTR_NIP42_MIN_CHALLENGE_LENGTH ||
|
||||
challenge_len >= NOSTR_NIP42_MAX_CHALLENGE_LENGTH) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -64,11 +64,13 @@ cJSON* nostr_nip42_create_auth_event_with_signer(const char* challenge,
|
||||
cJSON_AddItemToArray(challenge_tag, cJSON_CreateString(challenge));
|
||||
cJSON_AddItemToArray(tags, challenge_tag);
|
||||
|
||||
cJSON* auth_event = nostr_create_and_sign_event_with_signer(
|
||||
// Create authentication event using existing function
|
||||
// Note: Empty content as per NIP-42 specification
|
||||
cJSON* auth_event = nostr_create_and_sign_event(
|
||||
NOSTR_NIP42_AUTH_EVENT_KIND,
|
||||
"",
|
||||
"", // Empty content
|
||||
tags,
|
||||
signer,
|
||||
private_key,
|
||||
timestamp
|
||||
);
|
||||
|
||||
@@ -76,27 +78,6 @@ cJSON* nostr_nip42_create_auth_event_with_signer(const char* challenge,
|
||||
return auth_event;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip42_create_auth_event(const char* challenge,
|
||||
const char* relay_url,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
evt = nostr_nip42_create_auth_event_with_signer(challenge, relay_url, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create AUTH message JSON for relay communication
|
||||
*/
|
||||
|
||||
+1
-7
@@ -12,7 +12,6 @@
|
||||
#include <time.h>
|
||||
#include "../cjson/cJSON.h"
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_signer.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -65,16 +64,11 @@ typedef struct {
|
||||
* @param timestamp Event timestamp (0 for current time)
|
||||
* @return cJSON event object or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip42_create_auth_event(const char* challenge,
|
||||
cJSON* nostr_nip42_create_auth_event(const char* challenge,
|
||||
const char* relay_url,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
cJSON* nostr_nip42_create_auth_event_with_signer(const char* challenge,
|
||||
const char* relay_url,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
/**
|
||||
* Create AUTH message JSON for relay communication
|
||||
* @param auth_event Authentication event (kind 22242)
|
||||
|
||||
+40
-145
@@ -360,12 +360,12 @@ int nostr_nip46_parse_response(const char* json_payload, nostr_nip46_response_t*
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static cJSON* create_nip46_event_with_signer(int kind,
|
||||
const char* encrypted_content,
|
||||
nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp) {
|
||||
if (!encrypted_content || !signer || !recipient_public_key) return NULL;
|
||||
static cJSON* create_nip46_event(int kind,
|
||||
const char* encrypted_content,
|
||||
const unsigned char* sender_private_key,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp) {
|
||||
if (!encrypted_content || !sender_private_key || !recipient_public_key) return NULL;
|
||||
|
||||
char recipient_hex[65];
|
||||
nostr_bytes_to_hex(recipient_public_key, 32, recipient_hex);
|
||||
@@ -383,7 +383,7 @@ static cJSON* create_nip46_event_with_signer(int kind,
|
||||
cJSON_AddItemToArray(ptag, cJSON_CreateString(recipient_hex));
|
||||
cJSON_AddItemToArray(tags, ptag);
|
||||
|
||||
cJSON* evt = nostr_create_and_sign_event_with_signer(kind, encrypted_content, tags, signer, timestamp);
|
||||
cJSON* evt = nostr_create_and_sign_event(kind, encrypted_content, tags, sender_private_key, timestamp);
|
||||
cJSON_Delete(tags);
|
||||
return evt;
|
||||
}
|
||||
@@ -392,129 +392,48 @@ cJSON* nostr_nip46_create_request_event(const nostr_nip46_request_t* request,
|
||||
const unsigned char* sender_private_key,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* out;
|
||||
|
||||
if (!sender_private_key) return NULL;
|
||||
signer = nostr_signer_local(sender_private_key);
|
||||
if (!signer) return NULL;
|
||||
|
||||
out = nostr_nip46_create_request_event_with_signer(request, signer, recipient_public_key, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return out;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip46_create_request_event_with_signer(const nostr_nip46_request_t* request,
|
||||
nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp) {
|
||||
if (!request || !signer || !recipient_public_key) return NULL;
|
||||
if (!request || !sender_private_key || !recipient_public_key) return NULL;
|
||||
|
||||
char* json_payload = NULL;
|
||||
if (nostr_nip46_request_to_json(request, &json_payload) != NOSTR_SUCCESS) return NULL;
|
||||
|
||||
char recipient_hex[65];
|
||||
nostr_bytes_to_hex(recipient_public_key, 32, recipient_hex);
|
||||
|
||||
char* encrypted = NULL;
|
||||
int rc = nostr_signer_nip44_encrypt(signer, recipient_hex, json_payload, &encrypted);
|
||||
char encrypted[NOSTR_NIP46_MAX_PAYLOAD_LEN];
|
||||
int rc = nostr_nip44_encrypt(sender_private_key, recipient_public_key, json_payload,
|
||||
encrypted, sizeof(encrypted));
|
||||
free(json_payload);
|
||||
if (rc != NOSTR_SUCCESS || !encrypted) {
|
||||
free(encrypted);
|
||||
return NULL;
|
||||
}
|
||||
if (rc != NOSTR_SUCCESS) return NULL;
|
||||
|
||||
cJSON* out = create_nip46_event_with_signer(NOSTR_NIP46_EVENT_KIND, encrypted,
|
||||
signer, recipient_public_key, timestamp);
|
||||
free(encrypted);
|
||||
return out;
|
||||
return create_nip46_event(NOSTR_NIP46_EVENT_KIND, encrypted,
|
||||
sender_private_key, recipient_public_key, timestamp);
|
||||
}
|
||||
|
||||
cJSON* nostr_nip46_create_response_event(const nostr_nip46_response_t* response,
|
||||
const unsigned char* sender_private_key,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* out;
|
||||
|
||||
if (!sender_private_key) return NULL;
|
||||
signer = nostr_signer_local(sender_private_key);
|
||||
if (!signer) return NULL;
|
||||
|
||||
out = nostr_nip46_create_response_event_with_signer(response, signer, recipient_public_key, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return out;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip46_create_response_event_with_signer(const nostr_nip46_response_t* response,
|
||||
nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp) {
|
||||
if (!response || !signer || !recipient_public_key) return NULL;
|
||||
if (!response || !sender_private_key || !recipient_public_key) return NULL;
|
||||
|
||||
char* json_payload = NULL;
|
||||
if (nostr_nip46_response_to_json(response, &json_payload) != NOSTR_SUCCESS) return NULL;
|
||||
|
||||
char recipient_hex[65];
|
||||
nostr_bytes_to_hex(recipient_public_key, 32, recipient_hex);
|
||||
|
||||
char* encrypted = NULL;
|
||||
int rc = nostr_signer_nip44_encrypt(signer, recipient_hex, json_payload, &encrypted);
|
||||
char encrypted[NOSTR_NIP46_MAX_PAYLOAD_LEN];
|
||||
int rc = nostr_nip44_encrypt(sender_private_key, recipient_public_key, json_payload,
|
||||
encrypted, sizeof(encrypted));
|
||||
free(json_payload);
|
||||
if (rc != NOSTR_SUCCESS || !encrypted) {
|
||||
free(encrypted);
|
||||
return NULL;
|
||||
}
|
||||
if (rc != NOSTR_SUCCESS) return NULL;
|
||||
|
||||
cJSON* out = create_nip46_event_with_signer(NOSTR_NIP46_EVENT_KIND, encrypted,
|
||||
signer, recipient_public_key, timestamp);
|
||||
free(encrypted);
|
||||
return out;
|
||||
return create_nip46_event(NOSTR_NIP46_EVENT_KIND, encrypted,
|
||||
sender_private_key, recipient_public_key, timestamp);
|
||||
}
|
||||
|
||||
int nostr_nip46_decrypt_event(cJSON* event,
|
||||
const unsigned char* recipient_private_key,
|
||||
char* output,
|
||||
size_t output_size) {
|
||||
nostr_signer_t* signer;
|
||||
char* decrypted = NULL;
|
||||
int rc;
|
||||
|
||||
if (!recipient_private_key || !output || output_size == 0) {
|
||||
if (!event || !recipient_private_key || !output || output_size == 0) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(recipient_private_key);
|
||||
if (!signer) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
rc = nostr_nip46_decrypt_event_with_signer(event, signer, &decrypted);
|
||||
nostr_signer_free(signer);
|
||||
if (rc != NOSTR_SUCCESS || !decrypted) {
|
||||
free(decrypted);
|
||||
return NOSTR_ERROR_NIP46_DECRYPTION_FAILED;
|
||||
}
|
||||
|
||||
if (strlen(decrypted) + 1 > output_size) {
|
||||
free(decrypted);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
strcpy(output, decrypted);
|
||||
free(decrypted);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int nostr_nip46_decrypt_event_with_signer(cJSON* event,
|
||||
nostr_signer_t* signer,
|
||||
char** output_out) {
|
||||
if (!event || !signer || !output_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*output_out = NULL;
|
||||
|
||||
cJSON* content = cJSON_GetObjectItem(event, "content");
|
||||
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
|
||||
cJSON* kind = cJSON_GetObjectItem(event, "kind");
|
||||
@@ -527,10 +446,14 @@ int nostr_nip46_decrypt_event_with_signer(cJSON* event,
|
||||
return NOSTR_ERROR_NIP46_INVALID_REQUEST;
|
||||
}
|
||||
|
||||
int rc = nostr_signer_nip44_decrypt(signer,
|
||||
cJSON_GetStringValue(pubkey),
|
||||
cJSON_GetStringValue(content),
|
||||
output_out);
|
||||
const char* sender_hex = cJSON_GetStringValue(pubkey);
|
||||
unsigned char sender_pubkey[32];
|
||||
if (nostr_hex_to_bytes(sender_hex, sender_pubkey, 32) != 0) {
|
||||
return NOSTR_ERROR_NIP46_DECRYPTION_FAILED;
|
||||
}
|
||||
|
||||
int rc = nostr_nip44_decrypt(recipient_private_key, sender_pubkey,
|
||||
cJSON_GetStringValue(content), output, output_size);
|
||||
if (rc != NOSTR_SUCCESS) return NOSTR_ERROR_NIP46_DECRYPTION_FAILED;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
@@ -792,48 +715,23 @@ int nostr_nip46_create_nostrconnect_url(const nostr_nip46_nostrconnect_url_t* in
|
||||
int nostr_nip46_client_session_init(nostr_nip46_client_session_t* session,
|
||||
const unsigned char* client_private_key,
|
||||
const char* bunker_url) {
|
||||
nostr_signer_t* signer;
|
||||
int rc;
|
||||
|
||||
if (!client_private_key) return NOSTR_ERROR_INVALID_INPUT;
|
||||
signer = nostr_signer_local(client_private_key);
|
||||
if (!signer) return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
|
||||
rc = nostr_nip46_client_session_init_with_signer(session, signer, bunker_url);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
nostr_signer_free(signer);
|
||||
return rc;
|
||||
}
|
||||
|
||||
session->owns_client_signer = 1;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int nostr_nip46_client_session_init_with_signer(nostr_nip46_client_session_t* session,
|
||||
nostr_signer_t* signer,
|
||||
const char* bunker_url) {
|
||||
if (!session || !signer || !bunker_url) return NOSTR_ERROR_INVALID_INPUT;
|
||||
if (!session || !client_private_key || !bunker_url) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
memset(session, 0, sizeof(*session));
|
||||
session->client_signer = signer;
|
||||
session->owns_client_signer = 0;
|
||||
memcpy(session->client_private_key, client_private_key, 32);
|
||||
|
||||
int rc = nostr_signer_get_public_key(signer, session->client_pubkey_hex);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
memset(session, 0, sizeof(*session));
|
||||
unsigned char client_pub[32];
|
||||
if (nostr_ec_public_key_from_private_key(client_private_key, client_pub) != 0) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
nostr_bytes_to_hex(client_pub, 32, session->client_pubkey_hex);
|
||||
|
||||
nostr_nip46_bunker_url_t parsed;
|
||||
rc = nostr_nip46_parse_bunker_url(bunker_url, &parsed);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
memset(session, 0, sizeof(*session));
|
||||
return rc;
|
||||
}
|
||||
int rc = nostr_nip46_parse_bunker_url(bunker_url, &parsed);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
safe_copy(session->remote_signer_pubkey_hex, sizeof(session->remote_signer_pubkey_hex), parsed.remote_signer_pubkey);
|
||||
if (nostr_hex_to_bytes(parsed.remote_signer_pubkey, session->remote_signer_pubkey, 32) != 0) {
|
||||
memset(session, 0, sizeof(*session));
|
||||
return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
|
||||
}
|
||||
|
||||
@@ -848,9 +746,6 @@ int nostr_nip46_client_session_init_with_signer(nostr_nip46_client_session_t* se
|
||||
|
||||
void nostr_nip46_client_session_destroy(nostr_nip46_client_session_t* session) {
|
||||
if (!session) return;
|
||||
if (session->owns_client_signer && session->client_signer) {
|
||||
nostr_signer_free(session->client_signer);
|
||||
}
|
||||
memset(session, 0, sizeof(*session));
|
||||
}
|
||||
|
||||
@@ -859,7 +754,7 @@ static int client_make_request_event(nostr_nip46_client_session_t* session,
|
||||
const char** params,
|
||||
int param_count,
|
||||
cJSON** request_event_out) {
|
||||
if (!session || !session->client_signer || !request_event_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
if (!session || !request_event_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
char req_id[NOSTR_NIP46_MAX_REQUEST_ID_LEN];
|
||||
int rc = nostr_nip46_generate_request_id(req_id, sizeof(req_id));
|
||||
@@ -869,8 +764,8 @@ static int client_make_request_event(nostr_nip46_client_session_t* session,
|
||||
rc = nostr_nip46_create_request(req_id, method, params, param_count, &req);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
cJSON* evt = nostr_nip46_create_request_event_with_signer(&req, session->client_signer,
|
||||
session->remote_signer_pubkey, 0);
|
||||
cJSON* evt = nostr_nip46_create_request_event(&req, session->client_private_key,
|
||||
session->remote_signer_pubkey, 0);
|
||||
nostr_nip46_free_request(&req);
|
||||
if (!evt) return NOSTR_ERROR_NIP46_ENCRYPTION_FAILED;
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include <time.h>
|
||||
#include "nostr_common.h"
|
||||
#include "nip001.h"
|
||||
#include "nostr_signer.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
@@ -84,8 +83,6 @@ typedef struct {
|
||||
char relays[NOSTR_NIP46_MAX_RELAYS][NOSTR_NIP46_MAX_RELAY_URL_LEN];
|
||||
int relay_count;
|
||||
int connected;
|
||||
nostr_signer_t* client_signer;
|
||||
int owns_client_signer;
|
||||
} nostr_nip46_client_session_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -135,33 +132,19 @@ cJSON* nostr_nip46_create_request_event(const nostr_nip46_request_t* request,
|
||||
const unsigned char* sender_private_key,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip46_create_request_event_with_signer(const nostr_nip46_request_t* request,
|
||||
nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip46_create_response_event(const nostr_nip46_response_t* response,
|
||||
const unsigned char* sender_private_key,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip46_create_response_event_with_signer(const nostr_nip46_response_t* response,
|
||||
nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp);
|
||||
int nostr_nip46_decrypt_event(cJSON* event,
|
||||
const unsigned char* recipient_private_key,
|
||||
char* output,
|
||||
size_t output_size);
|
||||
int nostr_nip46_decrypt_event_with_signer(cJSON* event,
|
||||
nostr_signer_t* signer,
|
||||
char** output_out);
|
||||
|
||||
/* Client session */
|
||||
int nostr_nip46_client_session_init(nostr_nip46_client_session_t* session,
|
||||
const unsigned char* client_private_key,
|
||||
const char* bunker_url);
|
||||
int nostr_nip46_client_session_init_with_signer(nostr_nip46_client_session_t* session,
|
||||
nostr_signer_t* signer,
|
||||
const char* bunker_url);
|
||||
void nostr_nip46_client_session_destroy(nostr_nip46_client_session_t* session);
|
||||
|
||||
int nostr_nip46_client_connect(nostr_nip46_client_session_t* session,
|
||||
|
||||
+85
-163
@@ -41,39 +41,6 @@ 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
|
||||
*/
|
||||
@@ -182,79 +149,75 @@ cJSON* nostr_nip59_create_rumor(int kind, const char* content, cJSON* tags,
|
||||
*/
|
||||
cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private_key,
|
||||
const unsigned char* recipient_public_key, long max_delay_sec) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* out;
|
||||
|
||||
if (!sender_private_key) {
|
||||
if (!rumor || !sender_private_key || !recipient_public_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(sender_private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out = nostr_nip59_create_seal_with_signer(rumor, signer, recipient_public_key, max_delay_sec);
|
||||
nostr_signer_free(signer);
|
||||
return out;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip59_create_seal_with_signer(cJSON* rumor, nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key, long max_delay_sec) {
|
||||
cJSON* seal;
|
||||
cJSON* signed_seal = NULL;
|
||||
char* rumor_json;
|
||||
char recipient_pubkey_hex[65];
|
||||
char sender_pubkey_hex[65];
|
||||
char* encrypted_content = NULL;
|
||||
int rc;
|
||||
time_t seal_time;
|
||||
|
||||
if (!rumor || !signer || !recipient_public_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rumor_json = cJSON_PrintUnformatted(rumor);
|
||||
// Serialize the rumor to JSON
|
||||
char* rumor_json = cJSON_PrintUnformatted(rumor);
|
||||
if (!rumor_json) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(recipient_public_key, 32, recipient_pubkey_hex);
|
||||
rc = nostr_signer_nip44_encrypt(signer, recipient_pubkey_hex, rumor_json, &encrypted_content);
|
||||
// Encrypt the rumor using NIP-44
|
||||
char encrypted_content[4096]; // Should be large enough for most events
|
||||
int encrypt_result = nostr_nip44_encrypt(sender_private_key, recipient_public_key,
|
||||
rumor_json, encrypted_content, sizeof(encrypted_content));
|
||||
free(rumor_json);
|
||||
if (rc != NOSTR_SUCCESS || !encrypted_content) {
|
||||
free(encrypted_content);
|
||||
|
||||
if (encrypt_result != NOSTR_SUCCESS) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rc = nostr_signer_get_public_key(signer, sender_pubkey_hex);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
free(encrypted_content);
|
||||
// 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) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
seal = cJSON_CreateObject();
|
||||
char sender_pubkey_hex[65];
|
||||
nostr_bytes_to_hex(sender_public_key, 32, sender_pubkey_hex);
|
||||
|
||||
// Create seal event (kind 13)
|
||||
cJSON* seal = cJSON_CreateObject();
|
||||
if (!seal) {
|
||||
free(encrypted_content);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
seal_time = random_past_timestamp(max_delay_sec);
|
||||
time_t seal_time = random_past_timestamp(max_delay_sec);
|
||||
|
||||
cJSON_AddStringToObject(seal, "pubkey", sender_pubkey_hex);
|
||||
cJSON_AddNumberToObject(seal, "created_at", (double)seal_time);
|
||||
cJSON_AddNumberToObject(seal, "kind", 13);
|
||||
cJSON_AddItemToObject(seal, "tags", cJSON_CreateArray());
|
||||
cJSON_AddItemToObject(seal, "tags", cJSON_CreateArray()); // Empty tags array
|
||||
cJSON_AddStringToObject(seal, "content", encrypted_content);
|
||||
free(encrypted_content);
|
||||
|
||||
rc = nostr_signer_sign_event(signer, seal, &signed_seal);
|
||||
cJSON_Delete(seal);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(signed_seal);
|
||||
// Calculate event ID
|
||||
char event_id[65];
|
||||
if (create_event_id(seal, event_id) != 0) {
|
||||
cJSON_Delete(seal);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddStringToObject(seal, "id", event_id);
|
||||
|
||||
// Sign the seal
|
||||
unsigned char event_hash[32];
|
||||
if (nostr_hex_to_bytes(event_id, event_hash, 32) != 0) {
|
||||
cJSON_Delete(seal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return signed_seal;
|
||||
unsigned char signature[64];
|
||||
if (nostr_ec_sign(sender_private_key, event_hash, signature) != 0) {
|
||||
cJSON_Delete(seal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char sig_hex[129];
|
||||
nostr_bytes_to_hex(signature, 64, sig_hex);
|
||||
cJSON_AddStringToObject(seal, "sig", sig_hex);
|
||||
|
||||
return seal;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -298,27 +261,13 @@ cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_ke
|
||||
}
|
||||
|
||||
// Encrypt the seal using NIP-44
|
||||
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;
|
||||
}
|
||||
|
||||
char encrypted_content[8192]; // Larger buffer for nested encryption
|
||||
int encrypt_result = nostr_nip44_encrypt(random_private_key, recipient_public_key,
|
||||
seal_json, encrypted_content, encrypted_size);
|
||||
seal_json, encrypted_content, sizeof(encrypted_content));
|
||||
free(seal_json);
|
||||
|
||||
if (encrypt_result != NOSTR_SUCCESS) {
|
||||
memory_clear(random_private_key, 32);
|
||||
free(encrypted_content);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -326,7 +275,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -345,7 +293,6 @@ 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];
|
||||
@@ -385,53 +332,47 @@ cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_ke
|
||||
* NIP-59: Unwrap a gift wrap to get the seal
|
||||
*/
|
||||
cJSON* nostr_nip59_unwrap_gift(cJSON* gift_wrap, const unsigned char* recipient_private_key) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* out;
|
||||
|
||||
if (!recipient_private_key) {
|
||||
if (!gift_wrap || !recipient_private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(recipient_private_key);
|
||||
if (!signer) {
|
||||
// Get the encrypted content
|
||||
cJSON* content_item = cJSON_GetObjectItem(gift_wrap, "content");
|
||||
if (!content_item || !cJSON_IsString(content_item)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out = nostr_nip59_unwrap_gift_with_signer(gift_wrap, signer);
|
||||
nostr_signer_free(signer);
|
||||
return out;
|
||||
}
|
||||
const char* encrypted_content = cJSON_GetStringValue(content_item);
|
||||
|
||||
cJSON* nostr_nip59_unwrap_gift_with_signer(cJSON* gift_wrap, nostr_signer_t* signer) {
|
||||
cJSON* content_item;
|
||||
cJSON* pubkey_item;
|
||||
const char* encrypted_content;
|
||||
const char* sender_pubkey_hex;
|
||||
char* decrypted_json = NULL;
|
||||
int rc;
|
||||
cJSON* seal;
|
||||
|
||||
if (!gift_wrap || !signer) {
|
||||
// Get the sender's public key (gift wrap pubkey)
|
||||
cJSON* pubkey_item = cJSON_GetObjectItem(gift_wrap, "pubkey");
|
||||
if (!pubkey_item || !cJSON_IsString(pubkey_item)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
content_item = cJSON_GetObjectItem(gift_wrap, "content");
|
||||
pubkey_item = cJSON_GetObjectItem(gift_wrap, "pubkey");
|
||||
if (!content_item || !cJSON_IsString(content_item) || !pubkey_item || !cJSON_IsString(pubkey_item)) {
|
||||
const char* sender_pubkey_hex = cJSON_GetStringValue(pubkey_item);
|
||||
|
||||
// Convert sender pubkey hex to bytes
|
||||
unsigned char sender_public_key[32];
|
||||
if (nostr_hex_to_bytes(sender_pubkey_hex, sender_public_key, 32) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
encrypted_content = cJSON_GetStringValue(content_item);
|
||||
sender_pubkey_hex = cJSON_GetStringValue(pubkey_item);
|
||||
// Decrypt the content using NIP-44
|
||||
char decrypted_json[8192];
|
||||
int decrypt_result = nostr_nip44_decrypt(recipient_private_key, sender_public_key,
|
||||
encrypted_content, decrypted_json, sizeof(decrypted_json));
|
||||
|
||||
rc = nostr_signer_nip44_decrypt(signer, sender_pubkey_hex, encrypted_content, &decrypted_json);
|
||||
if (rc != NOSTR_SUCCESS || !decrypted_json) {
|
||||
free(decrypted_json);
|
||||
if (decrypt_result != NOSTR_SUCCESS) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Parse the decrypted JSON as the seal event
|
||||
cJSON* seal = cJSON_Parse(decrypted_json);
|
||||
if (!seal) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
seal = cJSON_Parse(decrypted_json);
|
||||
free(decrypted_json);
|
||||
return seal;
|
||||
}
|
||||
|
||||
@@ -440,51 +381,32 @@ cJSON* nostr_nip59_unwrap_gift_with_signer(cJSON* gift_wrap, nostr_signer_t* sig
|
||||
*/
|
||||
cJSON* nostr_nip59_unseal_rumor(cJSON* seal, const unsigned char* sender_public_key,
|
||||
const unsigned char* recipient_private_key) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* out;
|
||||
|
||||
if (!recipient_private_key) {
|
||||
if (!seal || !sender_public_key || !recipient_private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(recipient_private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out = nostr_nip59_unseal_rumor_with_signer(seal, sender_public_key, signer);
|
||||
nostr_signer_free(signer);
|
||||
return out;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip59_unseal_rumor_with_signer(cJSON* seal, const unsigned char* sender_public_key,
|
||||
nostr_signer_t* signer) {
|
||||
cJSON* content_item;
|
||||
const char* encrypted_content;
|
||||
char sender_pubkey_hex[65];
|
||||
char* decrypted_json = NULL;
|
||||
int rc;
|
||||
cJSON* rumor;
|
||||
|
||||
if (!seal || !sender_public_key || !signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
content_item = cJSON_GetObjectItem(seal, "content");
|
||||
// Get the encrypted content
|
||||
cJSON* content_item = cJSON_GetObjectItem(seal, "content");
|
||||
if (!content_item || !cJSON_IsString(content_item)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
encrypted_content = cJSON_GetStringValue(content_item);
|
||||
nostr_bytes_to_hex(sender_public_key, 32, sender_pubkey_hex);
|
||||
const char* encrypted_content = cJSON_GetStringValue(content_item);
|
||||
|
||||
rc = nostr_signer_nip44_decrypt(signer, sender_pubkey_hex, encrypted_content, &decrypted_json);
|
||||
if (rc != NOSTR_SUCCESS || !decrypted_json) {
|
||||
free(decrypted_json);
|
||||
// Decrypt the content using NIP-44
|
||||
char decrypted_json[4096];
|
||||
int decrypt_result = nostr_nip44_decrypt(recipient_private_key, sender_public_key,
|
||||
encrypted_content, decrypted_json, sizeof(decrypted_json));
|
||||
|
||||
if (decrypt_result != NOSTR_SUCCESS) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Parse the decrypted JSON as the rumor event
|
||||
cJSON* rumor = cJSON_Parse(decrypted_json);
|
||||
if (!rumor) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rumor = cJSON_Parse(decrypted_json);
|
||||
free(decrypted_json);
|
||||
return rumor;
|
||||
}
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
#include <stddef.h>
|
||||
#include <time.h>
|
||||
#include "nostr_signer.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
@@ -39,8 +38,6 @@ cJSON* nostr_nip59_create_rumor(int kind, const char* content, cJSON* tags,
|
||||
*/
|
||||
cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private_key,
|
||||
const unsigned char* recipient_public_key, long max_delay_sec);
|
||||
cJSON* nostr_nip59_create_seal_with_signer(cJSON* rumor, nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key, long max_delay_sec);
|
||||
|
||||
/**
|
||||
* NIP-59: Create a gift wrap (kind 1059) wrapping a seal
|
||||
@@ -60,7 +57,6 @@ cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_ke
|
||||
* @return cJSON object representing the seal event, or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip59_unwrap_gift(cJSON* gift_wrap, const unsigned char* recipient_private_key);
|
||||
cJSON* nostr_nip59_unwrap_gift_with_signer(cJSON* gift_wrap, nostr_signer_t* signer);
|
||||
|
||||
/**
|
||||
* NIP-59: Unseal a seal to get the rumor
|
||||
@@ -72,8 +68,6 @@ cJSON* nostr_nip59_unwrap_gift_with_signer(cJSON* gift_wrap, nostr_signer_t* sig
|
||||
*/
|
||||
cJSON* nostr_nip59_unseal_rumor(cJSON* seal, const unsigned char* sender_public_key,
|
||||
const unsigned char* recipient_private_key);
|
||||
cJSON* nostr_nip59_unseal_rumor_with_signer(cJSON* seal, const unsigned char* sender_public_key,
|
||||
nostr_signer_t* signer);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
+40
-185
@@ -47,56 +47,20 @@ static nostr_nip60_direction_t nip60_string_to_direction(const char* s) {
|
||||
return NOSTR_NIP60_DIRECTION_IN;
|
||||
}
|
||||
|
||||
static int nip60_encrypt_self_with_signer(nostr_signer_t* signer,
|
||||
const char* plaintext,
|
||||
char* output,
|
||||
size_t output_size) {
|
||||
if (!signer || !plaintext || !output || output_size == 0) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
char pubkey_hex[65];
|
||||
int rc = nostr_signer_get_public_key(signer, pubkey_hex);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
char* encrypted = NULL;
|
||||
rc = nostr_signer_nip44_encrypt(signer, pubkey_hex, plaintext, &encrypted);
|
||||
if (rc != NOSTR_SUCCESS || !encrypted) {
|
||||
free(encrypted);
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
if (strlen(encrypted) + 1 > output_size) {
|
||||
free(encrypted);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
strcpy(output, encrypted);
|
||||
free(encrypted);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int nip60_encrypt_self(const unsigned char* private_key,
|
||||
const char* plaintext,
|
||||
char* output,
|
||||
size_t output_size) {
|
||||
nostr_signer_t* signer;
|
||||
int rc;
|
||||
|
||||
if (!private_key) {
|
||||
if (!private_key || !plaintext || !output || output_size == 0) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
unsigned char pubkey[32];
|
||||
if (nostr_ec_public_key_from_private_key(private_key, pubkey) != 0) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
rc = nip60_encrypt_self_with_signer(signer, plaintext, output, output_size);
|
||||
nostr_signer_free(signer);
|
||||
return rc;
|
||||
return nostr_nip44_encrypt(private_key, pubkey, plaintext, output, output_size);
|
||||
}
|
||||
|
||||
static int nip60_decrypt_event_content(cJSON* event,
|
||||
@@ -241,10 +205,10 @@ int nostr_nip60_proofs_from_json(cJSON* json_array,
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_wallet_event_with_signer(const nostr_nip60_wallet_data_t* wallet_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!wallet_data || !signer || wallet_data->mint_count <= 0) {
|
||||
cJSON* nostr_nip60_create_wallet_event(const nostr_nip60_wallet_data_t* wallet_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
if (!wallet_data || !private_key || wallet_data->mint_count <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -277,31 +241,11 @@ cJSON* nostr_nip60_create_wallet_event_with_signer(const nostr_nip60_wallet_data
|
||||
if (!plain) return NULL;
|
||||
|
||||
char encrypted[65536];
|
||||
int rc = nip60_encrypt_self_with_signer(signer, plain, encrypted, sizeof(encrypted));
|
||||
int rc = nip60_encrypt_self(private_key, plain, encrypted, sizeof(encrypted));
|
||||
free(plain);
|
||||
if (rc != NOSTR_SUCCESS) return NULL;
|
||||
|
||||
return nostr_create_and_sign_event_with_signer(NOSTR_NIP60_WALLET_KIND, encrypted, NULL, signer, timestamp);
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_wallet_event(const nostr_nip60_wallet_data_t* wallet_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
evt = nostr_nip60_create_wallet_event_with_signer(wallet_data, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
return nostr_create_and_sign_event(NOSTR_NIP60_WALLET_KIND, encrypted, NULL, private_key, timestamp);
|
||||
}
|
||||
|
||||
int nostr_nip60_parse_wallet_event(cJSON* event,
|
||||
@@ -394,10 +338,10 @@ void nostr_nip60_free_wallet_data(nostr_nip60_wallet_data_t* data) {
|
||||
memset(data, 0, sizeof(*data));
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_token_event_with_signer(const nostr_nip60_token_data_t* token_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!token_data || !signer || !token_data->mint_url ||
|
||||
cJSON* nostr_nip60_create_token_event(const nostr_nip60_token_data_t* token_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
if (!token_data || !private_key || !token_data->mint_url ||
|
||||
!token_data->proofs || token_data->proof_count <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -433,31 +377,11 @@ cJSON* nostr_nip60_create_token_event_with_signer(const nostr_nip60_token_data_t
|
||||
if (!plain) return NULL;
|
||||
|
||||
char encrypted[131072];
|
||||
int rc = nip60_encrypt_self_with_signer(signer, plain, encrypted, sizeof(encrypted));
|
||||
int rc = nip60_encrypt_self(private_key, plain, encrypted, sizeof(encrypted));
|
||||
free(plain);
|
||||
if (rc != NOSTR_SUCCESS) return NULL;
|
||||
|
||||
return nostr_create_and_sign_event_with_signer(NOSTR_NIP60_TOKEN_KIND, encrypted, NULL, signer, timestamp);
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_token_event(const nostr_nip60_token_data_t* token_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
evt = nostr_nip60_create_token_event_with_signer(token_data, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
return nostr_create_and_sign_event(NOSTR_NIP60_TOKEN_KIND, encrypted, NULL, private_key, timestamp);
|
||||
}
|
||||
|
||||
int nostr_nip60_parse_token_event(cJSON* event,
|
||||
@@ -552,10 +476,10 @@ void nostr_nip60_free_token_data(nostr_nip60_token_data_t* data) {
|
||||
memset(data, 0, sizeof(*data));
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_token_deletion_with_signer(const char* token_event_id,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!token_event_id || !signer) return NULL;
|
||||
cJSON* nostr_nip60_create_token_deletion(const char* token_event_id,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
if (!token_event_id || !private_key) return NULL;
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
@@ -570,64 +494,29 @@ cJSON* nostr_nip60_create_token_deletion_with_signer(const char* token_event_id,
|
||||
cJSON_AddItemToArray(k_tag, cJSON_CreateString("7375"));
|
||||
cJSON_AddItemToArray(tags, k_tag);
|
||||
|
||||
cJSON* evt = nostr_create_and_sign_event_with_signer(5, "NIP-60 token spent", tags, signer, timestamp);
|
||||
cJSON* evt = nostr_create_and_sign_event(5, "NIP-60 token spent", tags, private_key, timestamp);
|
||||
cJSON_Delete(tags);
|
||||
return evt;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_token_deletion(const char* token_event_id,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) return NULL;
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) return NULL;
|
||||
|
||||
evt = nostr_nip60_create_token_deletion_with_signer(token_event_id, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_rollover_token_with_signer(const nostr_nip60_token_data_t* remaining_proofs,
|
||||
const char** deleted_event_ids,
|
||||
int deleted_count,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!remaining_proofs || !signer) return NULL;
|
||||
|
||||
nostr_nip60_token_data_t token = *remaining_proofs;
|
||||
token.deleted_token_ids = (char**)deleted_event_ids;
|
||||
token.deleted_count = deleted_count;
|
||||
|
||||
return nostr_nip60_create_token_event_with_signer(&token, signer, timestamp);
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_rollover_token(const nostr_nip60_token_data_t* remaining_proofs,
|
||||
const char** deleted_event_ids,
|
||||
int deleted_count,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
if (!remaining_proofs || !private_key) return NULL;
|
||||
|
||||
if (!private_key) return NULL;
|
||||
nostr_nip60_token_data_t token = *remaining_proofs;
|
||||
token.deleted_token_ids = (char**)deleted_event_ids;
|
||||
token.deleted_count = deleted_count;
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) return NULL;
|
||||
|
||||
evt = nostr_nip60_create_rollover_token_with_signer(remaining_proofs, deleted_event_ids, deleted_count,
|
||||
signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
return nostr_nip60_create_token_event(&token, private_key, timestamp);
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_history_event_with_signer(const nostr_nip60_history_data_t* history_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!history_data || !signer) return NULL;
|
||||
cJSON* nostr_nip60_create_history_event(const nostr_nip60_history_data_t* history_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
if (!history_data || !private_key) return NULL;
|
||||
|
||||
cJSON* payload = cJSON_CreateArray();
|
||||
if (!payload) return NULL;
|
||||
@@ -658,7 +547,7 @@ cJSON* nostr_nip60_create_history_event_with_signer(const nostr_nip60_history_da
|
||||
if (!plain) return NULL;
|
||||
|
||||
char encrypted[65536];
|
||||
int rc = nip60_encrypt_self_with_signer(signer, plain, encrypted, sizeof(encrypted));
|
||||
int rc = nip60_encrypt_self(private_key, plain, encrypted, sizeof(encrypted));
|
||||
free(plain);
|
||||
if (rc != NOSTR_SUCCESS) return NULL;
|
||||
|
||||
@@ -676,27 +565,11 @@ cJSON* nostr_nip60_create_history_event_with_signer(const nostr_nip60_history_da
|
||||
cJSON_AddItemToArray(tags, e);
|
||||
}
|
||||
|
||||
cJSON* evt = nostr_create_and_sign_event_with_signer(NOSTR_NIP60_HISTORY_KIND, encrypted, tags, signer, timestamp);
|
||||
cJSON* evt = nostr_create_and_sign_event(NOSTR_NIP60_HISTORY_KIND, encrypted, tags, private_key, timestamp);
|
||||
cJSON_Delete(tags);
|
||||
return evt;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_history_event(const nostr_nip60_history_data_t* history_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) return NULL;
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) return NULL;
|
||||
|
||||
evt = nostr_nip60_create_history_event_with_signer(history_data, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
int nostr_nip60_parse_history_event(cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
nostr_nip60_history_data_t* history_data_out) {
|
||||
@@ -792,17 +665,17 @@ void nostr_nip60_free_history_data(nostr_nip60_history_data_t* data) {
|
||||
memset(data, 0, sizeof(*data));
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_quote_event_with_signer(const char* quote_id,
|
||||
const char* mint_url,
|
||||
time_t expiration,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!quote_id || !mint_url || !signer || expiration <= 0) {
|
||||
cJSON* nostr_nip60_create_quote_event(const char* quote_id,
|
||||
const char* mint_url,
|
||||
time_t expiration,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
if (!quote_id || !mint_url || !private_key || expiration <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char encrypted[4096];
|
||||
int rc = nip60_encrypt_self_with_signer(signer, quote_id, encrypted, sizeof(encrypted));
|
||||
int rc = nip60_encrypt_self(private_key, quote_id, encrypted, sizeof(encrypted));
|
||||
if (rc != NOSTR_SUCCESS) return NULL;
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
@@ -821,29 +694,11 @@ cJSON* nostr_nip60_create_quote_event_with_signer(const char* quote_id,
|
||||
cJSON_AddItemToArray(mint_tag, cJSON_CreateString(mint_url));
|
||||
cJSON_AddItemToArray(tags, mint_tag);
|
||||
|
||||
cJSON* evt = nostr_create_and_sign_event_with_signer(NOSTR_NIP60_QUOTE_KIND, encrypted, tags, signer, timestamp);
|
||||
cJSON* evt = nostr_create_and_sign_event(NOSTR_NIP60_QUOTE_KIND, encrypted, tags, private_key, timestamp);
|
||||
cJSON_Delete(tags);
|
||||
return evt;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_quote_event(const char* quote_id,
|
||||
const char* mint_url,
|
||||
time_t expiration,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) return NULL;
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) return NULL;
|
||||
|
||||
evt = nostr_nip60_create_quote_event_with_signer(quote_id, mint_url, expiration, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
int nostr_nip60_parse_quote_event(cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
char* quote_id_out,
|
||||
|
||||
+1
-24
@@ -11,7 +11,6 @@
|
||||
#include <time.h>
|
||||
#include "nip001.h"
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_signer.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
@@ -23,7 +22,7 @@ extern "C" {
|
||||
#define NOSTR_NIP60_HISTORY_KIND 7376
|
||||
#define NOSTR_NIP60_QUOTE_KIND 7374
|
||||
|
||||
#define NOSTR_CASHU_KEYSET_ID_HEX_SIZE 65
|
||||
#define NOSTR_CASHU_KEYSET_ID_HEX_SIZE 17
|
||||
#define NOSTR_CASHU_PUBKEY_HEX_SIZE 67
|
||||
#define NOSTR_CASHU_EVENT_ID_HEX_SIZE 65
|
||||
|
||||
@@ -75,9 +74,6 @@ typedef struct {
|
||||
cJSON* nostr_nip60_create_wallet_event(const nostr_nip60_wallet_data_t* wallet_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_wallet_event_with_signer(const nostr_nip60_wallet_data_t* wallet_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
int nostr_nip60_parse_wallet_event(cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
@@ -88,9 +84,6 @@ void nostr_nip60_free_wallet_data(nostr_nip60_wallet_data_t* data);
|
||||
cJSON* nostr_nip60_create_token_event(const nostr_nip60_token_data_t* token_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_token_event_with_signer(const nostr_nip60_token_data_t* token_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
int nostr_nip60_parse_token_event(cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
@@ -101,27 +94,16 @@ void nostr_nip60_free_token_data(nostr_nip60_token_data_t* data);
|
||||
cJSON* nostr_nip60_create_token_deletion(const char* token_event_id,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_token_deletion_with_signer(const char* token_event_id,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
cJSON* nostr_nip60_create_rollover_token(const nostr_nip60_token_data_t* remaining_proofs,
|
||||
const char** deleted_event_ids,
|
||||
int deleted_count,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_rollover_token_with_signer(const nostr_nip60_token_data_t* remaining_proofs,
|
||||
const char** deleted_event_ids,
|
||||
int deleted_count,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
cJSON* nostr_nip60_create_history_event(const nostr_nip60_history_data_t* history_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_history_event_with_signer(const nostr_nip60_history_data_t* history_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
int nostr_nip60_parse_history_event(cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
@@ -134,11 +116,6 @@ cJSON* nostr_nip60_create_quote_event(const char* quote_id,
|
||||
time_t expiration,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_quote_event_with_signer(const char* quote_id,
|
||||
const char* mint_url,
|
||||
time_t expiration,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
int nostr_nip60_parse_quote_event(cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
|
||||
+20
-84
@@ -28,10 +28,10 @@ static int nip61_mint_in_info(const char* mint_url, const nostr_nip61_nutzap_inf
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip61_create_nutzap_info_event_with_signer(const nostr_nip61_nutzap_info_t* info,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!info || !signer || info->mint_count <= 0 || info->relay_count <= 0 || info->pubkey[0] == '\0') {
|
||||
cJSON* nostr_nip61_create_nutzap_info_event(const nostr_nip61_nutzap_info_t* info,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
if (!info || !private_key || info->mint_count <= 0 || info->relay_count <= 0 || info->pubkey[0] == '\0') {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -64,27 +64,11 @@ cJSON* nostr_nip61_create_nutzap_info_event_with_signer(const nostr_nip61_nutzap
|
||||
cJSON_AddItemToArray(pub, cJSON_CreateString(info->pubkey));
|
||||
cJSON_AddItemToArray(tags, pub);
|
||||
|
||||
cJSON* evt = nostr_create_and_sign_event_with_signer(NOSTR_NIP61_NUTZAP_INFO_KIND, "", tags, signer, timestamp);
|
||||
cJSON* evt = nostr_create_and_sign_event(NOSTR_NIP61_NUTZAP_INFO_KIND, "", tags, private_key, timestamp);
|
||||
cJSON_Delete(tags);
|
||||
return evt;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip61_create_nutzap_info_event(const nostr_nip61_nutzap_info_t* info,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) return NULL;
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) return NULL;
|
||||
|
||||
evt = nostr_nip61_create_nutzap_info_event_with_signer(info, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
int nostr_nip61_parse_nutzap_info_event(cJSON* event,
|
||||
nostr_nip61_nutzap_info_t* info_out) {
|
||||
if (!event || !info_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
@@ -220,10 +204,10 @@ void nostr_nip61_free_nutzap_info(nostr_nip61_nutzap_info_t* info) {
|
||||
memset(info, 0, sizeof(*info));
|
||||
}
|
||||
|
||||
cJSON* nostr_nip61_create_nutzap_event_with_signer(const nostr_nip61_nutzap_data_t* nutzap_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!nutzap_data || !signer || !nutzap_data->mint_url ||
|
||||
cJSON* nostr_nip61_create_nutzap_event(const nostr_nip61_nutzap_data_t* nutzap_data,
|
||||
const unsigned char* sender_private_key,
|
||||
time_t timestamp) {
|
||||
if (!nutzap_data || !sender_private_key || !nutzap_data->mint_url ||
|
||||
!nutzap_data->proofs || nutzap_data->proof_count <= 0 ||
|
||||
nutzap_data->recipient_pubkey[0] == '\0') {
|
||||
return NULL;
|
||||
@@ -281,27 +265,11 @@ cJSON* nostr_nip61_create_nutzap_event_with_signer(const nostr_nip61_nutzap_data
|
||||
}
|
||||
|
||||
const char* content = nutzap_data->content ? nutzap_data->content : "";
|
||||
cJSON* evt = nostr_create_and_sign_event_with_signer(NOSTR_NIP61_NUTZAP_KIND, content, tags, signer, timestamp);
|
||||
cJSON* evt = nostr_create_and_sign_event(NOSTR_NIP61_NUTZAP_KIND, content, tags, sender_private_key, timestamp);
|
||||
cJSON_Delete(tags);
|
||||
return evt;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip61_create_nutzap_event(const nostr_nip61_nutzap_data_t* nutzap_data,
|
||||
const unsigned char* sender_private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!sender_private_key) return NULL;
|
||||
|
||||
signer = nostr_signer_local(sender_private_key);
|
||||
if (!signer) return NULL;
|
||||
|
||||
evt = nostr_nip61_create_nutzap_event_with_signer(nutzap_data, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
int nostr_nip61_parse_nutzap_event(cJSON* event,
|
||||
nostr_nip61_nutzap_data_t* nutzap_data_out) {
|
||||
if (!event || !nutzap_data_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
@@ -424,15 +392,15 @@ void nostr_nip61_free_nutzap_data(nostr_nip61_nutzap_data_t* data) {
|
||||
memset(data, 0, sizeof(*data));
|
||||
}
|
||||
|
||||
cJSON* nostr_nip61_create_redemption_event_with_signer(const char* nutzap_event_id,
|
||||
const char* nutzap_relay_hint,
|
||||
const char* sender_pubkey,
|
||||
const char* created_token_event_id,
|
||||
const char* created_token_relay_hint,
|
||||
uint64_t amount,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!nutzap_event_id || !sender_pubkey || !created_token_event_id || !signer) {
|
||||
cJSON* nostr_nip61_create_redemption_event(const char* nutzap_event_id,
|
||||
const char* nutzap_relay_hint,
|
||||
const char* sender_pubkey,
|
||||
const char* created_token_event_id,
|
||||
const char* created_token_relay_hint,
|
||||
uint64_t amount,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
if (!nutzap_event_id || !sender_pubkey || !created_token_event_id || !private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -451,7 +419,7 @@ cJSON* nostr_nip61_create_redemption_event_with_signer(const char* nutzap_event_
|
||||
hist.refs = refs;
|
||||
hist.ref_count = 1;
|
||||
|
||||
cJSON* evt = nostr_nip60_create_history_event_with_signer(&hist, signer, timestamp);
|
||||
cJSON* evt = nostr_nip60_create_history_event(&hist, private_key, timestamp);
|
||||
if (!evt) return NULL;
|
||||
|
||||
cJSON* tags = cJSON_GetObjectItem(evt, "tags");
|
||||
@@ -472,38 +440,6 @@ cJSON* nostr_nip61_create_redemption_event_with_signer(const char* nutzap_event_
|
||||
return evt;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip61_create_redemption_event(const char* nutzap_event_id,
|
||||
const char* nutzap_relay_hint,
|
||||
const char* sender_pubkey,
|
||||
const char* created_token_event_id,
|
||||
const char* created_token_relay_hint,
|
||||
uint64_t amount,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
evt = nostr_nip61_create_redemption_event_with_signer(nutzap_event_id,
|
||||
nutzap_relay_hint,
|
||||
sender_pubkey,
|
||||
created_token_event_id,
|
||||
created_token_relay_hint,
|
||||
amount,
|
||||
signer,
|
||||
timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
int nostr_nip61_verify_nutzap(cJSON* nutzap_event, cJSON* nutzap_info_event) {
|
||||
if (!nutzap_event || !nutzap_info_event) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include "nip001.h"
|
||||
#include "nip060.h"
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_signer.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
@@ -50,9 +49,6 @@ typedef struct {
|
||||
cJSON* nostr_nip61_create_nutzap_info_event(const nostr_nip61_nutzap_info_t* info,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip61_create_nutzap_info_event_with_signer(const nostr_nip61_nutzap_info_t* info,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
int nostr_nip61_parse_nutzap_info_event(cJSON* event,
|
||||
nostr_nip61_nutzap_info_t* info_out);
|
||||
@@ -62,9 +58,6 @@ void nostr_nip61_free_nutzap_info(nostr_nip61_nutzap_info_t* info);
|
||||
cJSON* nostr_nip61_create_nutzap_event(const nostr_nip61_nutzap_data_t* nutzap_data,
|
||||
const unsigned char* sender_private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip61_create_nutzap_event_with_signer(const nostr_nip61_nutzap_data_t* nutzap_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
int nostr_nip61_parse_nutzap_event(cJSON* event,
|
||||
nostr_nip61_nutzap_data_t* nutzap_data_out);
|
||||
@@ -79,14 +72,6 @@ cJSON* nostr_nip61_create_redemption_event(const char* nutzap_event_id,
|
||||
uint64_t amount,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip61_create_redemption_event_with_signer(const char* nutzap_event_id,
|
||||
const char* nutzap_relay_hint,
|
||||
const char* sender_pubkey,
|
||||
const char* created_token_event_id,
|
||||
const char* created_token_relay_hint,
|
||||
uint64_t amount,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
int nostr_nip61_verify_nutzap(cJSON* nutzap_event, cJSON* nutzap_info_event);
|
||||
|
||||
|
||||
@@ -28,9 +28,6 @@
|
||||
#define NOSTR_ERROR_NIP05_NAME_NOT_FOUND -19
|
||||
#define NOSTR_ERROR_NIP05_PUBKEY_MISMATCH -20
|
||||
#define NOSTR_ERROR_EVENT_INVALID_STRUCTURE -30
|
||||
/* nsigner remote backend errors */
|
||||
#define NOSTR_ERROR_NSIGNER_POLICY_DENIED -2001
|
||||
#define NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED -2002
|
||||
#define NOSTR_ERROR_EVENT_INVALID_ID -31
|
||||
#define NOSTR_ERROR_EVENT_INVALID_PUBKEY -32
|
||||
#define NOSTR_ERROR_EVENT_INVALID_SIGNATURE -33
|
||||
|
||||
+10
-28
@@ -2,10 +2,10 @@
|
||||
#define NOSTR_CORE_H
|
||||
|
||||
// Version information (auto-updated by increment_and_push.sh)
|
||||
#define VERSION "v0.6.11"
|
||||
#define VERSION "v0.5.2"
|
||||
#define VERSION_MAJOR 0
|
||||
#define VERSION_MINOR 6
|
||||
#define VERSION_PATCH 11
|
||||
#define VERSION_MINOR 5
|
||||
#define VERSION_PATCH 2
|
||||
|
||||
/*
|
||||
* NOSTR Core Library - Complete API Reference
|
||||
@@ -174,11 +174,9 @@ extern "C" {
|
||||
// Core common definitions and utilities
|
||||
#include "nostr_common.h"
|
||||
#include "utils.h"
|
||||
#include "nostr_signer.h"
|
||||
|
||||
// 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
|
||||
@@ -195,9 +193,6 @@ 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
|
||||
|
||||
@@ -246,14 +241,13 @@ 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 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
|
||||
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
|
||||
} nostr_pool_reconnect_config_t;
|
||||
|
||||
// Relay pool management functions
|
||||
@@ -262,7 +256,6 @@ nostr_pool_reconnect_config_t* nostr_pool_reconnect_config_default(void);
|
||||
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url);
|
||||
int nostr_relay_pool_remove_relay(nostr_relay_pool_t* pool, const char* relay_url);
|
||||
int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool, const unsigned char* private_key, int enable);
|
||||
int nostr_relay_pool_set_auth_with_signer(nostr_relay_pool_t* pool, nostr_signer_t* signer, int enable);
|
||||
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
|
||||
|
||||
// Subscription management
|
||||
@@ -304,17 +297,6 @@ cJSON** nostr_relay_pool_query_sync(
|
||||
cJSON* filter,
|
||||
int* event_count,
|
||||
int timeout_ms);
|
||||
// Same as above but also reports whether all relays sent EOSE (vs timed out).
|
||||
// out_got_eose is set to 1 if all connected relays sent EOSE, 0 if any timed
|
||||
// out. May be NULL if the caller doesn't care.
|
||||
cJSON** nostr_relay_pool_query_sync_with_eose(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
int* event_count,
|
||||
int timeout_ms,
|
||||
int* out_got_eose);
|
||||
cJSON* nostr_relay_pool_get_event(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* 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 */
|
||||
@@ -1,20 +0,0 @@
|
||||
#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 */
|
||||
@@ -1,880 +0,0 @@
|
||||
#include "nostr_signer.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "utils.h"
|
||||
#include "nip004.h"
|
||||
#include "nip044.h"
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
NOSTR_SIGNER_BACKEND_LOCAL = 1,
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE = 2,
|
||||
#endif
|
||||
} nostr_signer_backend_t;
|
||||
|
||||
struct nostr_signer {
|
||||
nostr_signer_backend_t backend;
|
||||
union {
|
||||
struct {
|
||||
unsigned char private_key[32];
|
||||
} local;
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
struct {
|
||||
nsigner_client_t* client;
|
||||
char role[64];
|
||||
int nostr_index; /* -1 = not set; when set, overrides role */
|
||||
int has_nostr_index; /* 1 if nostr_index is set */
|
||||
} remote;
|
||||
#endif
|
||||
} u;
|
||||
};
|
||||
|
||||
static int signer_hex_pubkey_to_bytes(const char* hex, unsigned char out[32]) {
|
||||
if (!hex || strlen(hex) != 64 || !out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
if (nostr_hex_to_bytes(hex, out, 32) != 0) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_local_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]) {
|
||||
unsigned char pubkey[32];
|
||||
|
||||
if (!signer || !out_pubkey_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (nostr_ec_public_key_from_private_key(signer->u.local.private_key, pubkey) != 0) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(pubkey, 32, out_pubkey_hex);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_local_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out) {
|
||||
cJSON* pubkey_item;
|
||||
cJSON* created_at_item;
|
||||
cJSON* kind_item;
|
||||
cJSON* tags_item;
|
||||
cJSON* content_item;
|
||||
cJSON* serialize_array;
|
||||
cJSON* signed_event = NULL;
|
||||
char* serialize_string = NULL;
|
||||
unsigned char event_hash[32];
|
||||
unsigned char signature[64];
|
||||
char event_id[65];
|
||||
char sig_hex[129];
|
||||
|
||||
if (!signer || !unsigned_event || !signed_event_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*signed_event_out = NULL;
|
||||
|
||||
pubkey_item = cJSON_GetObjectItem((cJSON*)unsigned_event, "pubkey");
|
||||
created_at_item = cJSON_GetObjectItem((cJSON*)unsigned_event, "created_at");
|
||||
kind_item = cJSON_GetObjectItem((cJSON*)unsigned_event, "kind");
|
||||
tags_item = cJSON_GetObjectItem((cJSON*)unsigned_event, "tags");
|
||||
content_item = cJSON_GetObjectItem((cJSON*)unsigned_event, "content");
|
||||
|
||||
if (!pubkey_item || !created_at_item || !kind_item || !tags_item || !content_item) {
|
||||
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
|
||||
}
|
||||
|
||||
serialize_array = cJSON_CreateArray();
|
||||
if (!serialize_array) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(serialize_array, cJSON_CreateNumber(0));
|
||||
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(pubkey_item, 1));
|
||||
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(created_at_item, 1));
|
||||
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(kind_item, 1));
|
||||
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(tags_item, 1));
|
||||
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(content_item, 1));
|
||||
|
||||
serialize_string = cJSON_PrintUnformatted(serialize_array);
|
||||
cJSON_Delete(serialize_array);
|
||||
|
||||
if (!serialize_string) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
if (nostr_sha256((const unsigned char*)serialize_string, strlen(serialize_string), event_hash) != 0) {
|
||||
free(serialize_string);
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(event_hash, 32, event_id);
|
||||
|
||||
if (nostr_ec_sign(signer->u.local.private_key, event_hash, signature) != 0) {
|
||||
free(serialize_string);
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(signature, 64, sig_hex);
|
||||
|
||||
signed_event = cJSON_Duplicate((cJSON*)unsigned_event, 1);
|
||||
if (!signed_event) {
|
||||
free(serialize_string);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(signed_event, "id");
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(signed_event, "sig");
|
||||
cJSON_AddStringToObject(signed_event, "id", event_id);
|
||||
cJSON_AddStringToObject(signed_event, "sig", sig_hex);
|
||||
|
||||
*signed_event_out = signed_event;
|
||||
free(serialize_string);
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_local_nip04_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out) {
|
||||
unsigned char peer_pubkey[32];
|
||||
char* out;
|
||||
int rc;
|
||||
|
||||
if (!signer || !peer_pubkey_hex || !plaintext || !ciphertext_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*ciphertext_out = NULL;
|
||||
|
||||
rc = signer_hex_pubkey_to_bytes(peer_pubkey_hex, peer_pubkey);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
out = (char*)malloc(NOSTR_NIP04_MAX_ENCRYPTED_SIZE + 1);
|
||||
if (!out) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = nostr_nip04_encrypt(signer->u.local.private_key, peer_pubkey, plaintext, out, NOSTR_NIP04_MAX_ENCRYPTED_SIZE + 1);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
free(out);
|
||||
return rc;
|
||||
}
|
||||
|
||||
*ciphertext_out = out;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_local_nip04_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out) {
|
||||
unsigned char peer_pubkey[32];
|
||||
char* out;
|
||||
int rc;
|
||||
|
||||
if (!signer || !peer_pubkey_hex || !ciphertext || !plaintext_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*plaintext_out = NULL;
|
||||
|
||||
rc = signer_hex_pubkey_to_bytes(peer_pubkey_hex, peer_pubkey);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
out = (char*)malloc(NOSTR_NIP04_MAX_PLAINTEXT_SIZE + 1);
|
||||
if (!out) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = nostr_nip04_decrypt(signer->u.local.private_key, peer_pubkey, ciphertext, out, NOSTR_NIP04_MAX_PLAINTEXT_SIZE + 1);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
free(out);
|
||||
return rc;
|
||||
}
|
||||
|
||||
*plaintext_out = out;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_local_nip44_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out) {
|
||||
unsigned char peer_pubkey[32];
|
||||
char* out;
|
||||
size_t out_size;
|
||||
int rc;
|
||||
|
||||
if (!signer || !peer_pubkey_hex || !plaintext || !ciphertext_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*ciphertext_out = NULL;
|
||||
|
||||
rc = signer_hex_pubkey_to_bytes(peer_pubkey_hex, peer_pubkey);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
out_size = (strlen(plaintext) * 4) + 4096;
|
||||
if (out_size < 8192) {
|
||||
out_size = 8192;
|
||||
}
|
||||
|
||||
out = (char*)malloc(out_size);
|
||||
if (!out) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = nostr_nip44_encrypt(signer->u.local.private_key, peer_pubkey, plaintext, out, out_size);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
free(out);
|
||||
return rc;
|
||||
}
|
||||
|
||||
*ciphertext_out = out;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_local_nip44_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out) {
|
||||
unsigned char peer_pubkey[32];
|
||||
char* out;
|
||||
int rc;
|
||||
|
||||
if (!signer || !peer_pubkey_hex || !ciphertext || !plaintext_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*plaintext_out = NULL;
|
||||
|
||||
rc = signer_hex_pubkey_to_bytes(peer_pubkey_hex, peer_pubkey);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
out = (char*)malloc(NOSTR_NIP44_MAX_PLAINTEXT_SIZE + 1);
|
||||
if (!out) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = nostr_nip44_decrypt(signer->u.local.private_key, peer_pubkey, ciphertext, out, NOSTR_NIP44_MAX_PLAINTEXT_SIZE + 1);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
free(out);
|
||||
return rc;
|
||||
}
|
||||
|
||||
*plaintext_out = out;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_local_derive_hmac(nostr_signer_t* signer,
|
||||
const char* data,
|
||||
char out_digest_hex[65]) {
|
||||
unsigned char mac[32];
|
||||
|
||||
if (!signer || !data || !out_digest_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (nostr_hmac_sha256(signer->u.local.private_key, 32,
|
||||
(const unsigned char*)data, strlen(data),
|
||||
mac) != 0) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(mac, 32, out_digest_hex);
|
||||
memset(mac, 0, sizeof(mac));
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
|
||||
static cJSON* signer_remote_params_with_selector(cJSON* params, const char* role, int has_nostr_index, int nostr_index) {
|
||||
cJSON* selector;
|
||||
|
||||
if (params == NULL) {
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* nostr_index takes precedence over role when set */
|
||||
if (has_nostr_index) {
|
||||
selector = cJSON_CreateObject();
|
||||
if (selector == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddNumberToObject(selector, "nostr_index", nostr_index);
|
||||
cJSON_AddItemToArray(params, selector);
|
||||
return params;
|
||||
}
|
||||
|
||||
if (role == NULL || role[0] == '\0') {
|
||||
return params;
|
||||
}
|
||||
|
||||
selector = cJSON_CreateObject();
|
||||
if (selector == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(selector, "role", role);
|
||||
cJSON_AddItemToArray(params, selector);
|
||||
return params;
|
||||
}
|
||||
|
||||
static int signer_remote_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]) {
|
||||
cJSON* params;
|
||||
cJSON* result = NULL;
|
||||
const char* result_str;
|
||||
int rc;
|
||||
|
||||
if (!signer || !out_pubkey_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
params = signer_remote_params_with_selector(NULL, signer->u.remote.role,
|
||||
signer->u.remote.has_nostr_index,
|
||||
signer->u.remote.nostr_index);
|
||||
if (params == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = nsigner_client_call(signer->u.remote.client, "nostr_get_public_key", params, &result);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (!cJSON_IsString(result) || result->valuestring == NULL || strlen(result->valuestring) != 64) {
|
||||
cJSON_Delete(result);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
result_str = result->valuestring;
|
||||
memcpy(out_pubkey_hex, result_str, 64);
|
||||
out_pubkey_hex[64] = '\0';
|
||||
|
||||
cJSON_Delete(result);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_remote_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out) {
|
||||
cJSON* params;
|
||||
cJSON* result = NULL;
|
||||
cJSON* parsed = NULL;
|
||||
char* event_json;
|
||||
int rc;
|
||||
|
||||
if (!signer || !unsigned_event || !signed_event_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*signed_event_out = NULL;
|
||||
|
||||
event_json = cJSON_PrintUnformatted((cJSON*)unsigned_event);
|
||||
if (event_json == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) {
|
||||
free(event_json);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(event_json));
|
||||
free(event_json);
|
||||
|
||||
params = signer_remote_params_with_selector(params, signer->u.remote.role,
|
||||
signer->u.remote.has_nostr_index,
|
||||
signer->u.remote.nostr_index);
|
||||
if (params == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = nsigner_client_call(signer->u.remote.client, "nostr_sign_event", params, &result);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (cJSON_IsString(result) && result->valuestring != NULL) {
|
||||
parsed = cJSON_Parse(result->valuestring);
|
||||
cJSON_Delete(result);
|
||||
if (parsed == NULL || !cJSON_IsObject(parsed)) {
|
||||
cJSON_Delete(parsed);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
*signed_event_out = parsed;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
if (cJSON_IsObject(result)) {
|
||||
*signed_event_out = cJSON_Duplicate(result, 1);
|
||||
cJSON_Delete(result);
|
||||
if (*signed_event_out == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
cJSON_Delete(result);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
static int signer_remote_encrypt_decrypt(nostr_signer_t* signer,
|
||||
const char* method,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* in,
|
||||
char** out) {
|
||||
cJSON* params;
|
||||
cJSON* result = NULL;
|
||||
const char* result_str;
|
||||
int rc;
|
||||
|
||||
if (!signer || !method || !peer_pubkey_hex || !in || !out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*out = NULL;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(peer_pubkey_hex));
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(in));
|
||||
|
||||
params = signer_remote_params_with_selector(params, signer->u.remote.role,
|
||||
signer->u.remote.has_nostr_index,
|
||||
signer->u.remote.nostr_index);
|
||||
if (params == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = nsigner_client_call(signer->u.remote.client, method, params, &result);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (!cJSON_IsString(result) || result->valuestring == NULL) {
|
||||
cJSON_Delete(result);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
result_str = result->valuestring;
|
||||
{
|
||||
size_t n = strlen(result_str);
|
||||
*out = (char*)malloc(n + 1);
|
||||
if (*out == NULL) {
|
||||
cJSON_Delete(result);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(*out, result_str, n + 1);
|
||||
}
|
||||
cJSON_Delete(result);
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_remote_nip04_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out) {
|
||||
return signer_remote_encrypt_decrypt(signer, "nostr_nip04_encrypt", peer_pubkey_hex, plaintext, ciphertext_out);
|
||||
}
|
||||
|
||||
static int signer_remote_nip04_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out) {
|
||||
return signer_remote_encrypt_decrypt(signer, "nostr_nip04_decrypt", peer_pubkey_hex, ciphertext, plaintext_out);
|
||||
}
|
||||
|
||||
static int signer_remote_nip44_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out) {
|
||||
return signer_remote_encrypt_decrypt(signer, "nostr_nip44_encrypt", peer_pubkey_hex, plaintext, ciphertext_out);
|
||||
}
|
||||
|
||||
static int signer_remote_nip44_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out) {
|
||||
return signer_remote_encrypt_decrypt(signer, "nostr_nip44_decrypt", peer_pubkey_hex, ciphertext, plaintext_out);
|
||||
}
|
||||
|
||||
static int signer_remote_derive_hmac(nostr_signer_t* signer,
|
||||
const char* data,
|
||||
char out_digest_hex[65]) {
|
||||
cJSON* params;
|
||||
cJSON* opts = NULL;
|
||||
cJSON* result = NULL;
|
||||
cJSON* parsed = NULL;
|
||||
cJSON* digest_item = NULL;
|
||||
const char* digest_str = NULL;
|
||||
int rc;
|
||||
|
||||
if (!signer || !data || !out_digest_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(data));
|
||||
|
||||
/* Build the options object with algorithm:"secp256k1" and the selector
|
||||
* (role or nostr_index). The derive verb requires index, so when
|
||||
* has_nostr_index is set we include it; otherwise we include role (the
|
||||
* nsigner derive handler requires index, so callers using the remote
|
||||
* backend must set nostr_index via nostr_signer_nsigner_set_nostr_index
|
||||
* before calling derive_hmac). */
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
cJSON_AddStringToObject(opts, "algorithm", "secp256k1");
|
||||
if (signer->u.remote.has_nostr_index) {
|
||||
cJSON_AddNumberToObject(opts, "index", signer->u.remote.nostr_index);
|
||||
} else if (signer->u.remote.role[0] != '\0') {
|
||||
cJSON_AddStringToObject(opts, "role", signer->u.remote.role);
|
||||
}
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
|
||||
rc = nsigner_client_call(signer->u.remote.client, "derive", params, &result);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* result is a JSON string: {"algorithm":"secp256k1","key_id":"...","digest":"<64hex>"} */
|
||||
if (!cJSON_IsString(result) || result->valuestring == NULL) {
|
||||
cJSON_Delete(result);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
parsed = cJSON_Parse(result->valuestring);
|
||||
cJSON_Delete(result);
|
||||
if (parsed == NULL) {
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
digest_item = cJSON_GetObjectItemCaseSensitive(parsed, "digest");
|
||||
if (!cJSON_IsString(digest_item) || digest_item->valuestring == NULL ||
|
||||
strlen(digest_item->valuestring) != 64) {
|
||||
cJSON_Delete(parsed);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
digest_str = digest_item->valuestring;
|
||||
memcpy(out_digest_hex, digest_str, 64);
|
||||
out_digest_hex[64] = '\0';
|
||||
cJSON_Delete(parsed);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
#endif /* NOSTR_ENABLE_NSIGNER_CLIENT */
|
||||
|
||||
nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]) {
|
||||
nostr_signer_t* signer;
|
||||
|
||||
if (!private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = (nostr_signer_t*)calloc(1, sizeof(nostr_signer_t));
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer->backend = NOSTR_SIGNER_BACKEND_LOCAL;
|
||||
memcpy(signer->u.local.private_key, private_key, 32);
|
||||
return signer;
|
||||
}
|
||||
|
||||
void nostr_signer_free(nostr_signer_t* signer) {
|
||||
if (!signer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
memset(signer->u.local.private_key, 0, sizeof(signer->u.local.private_key));
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
else if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
if (signer->u.remote.client != NULL) {
|
||||
nsigner_client_free(signer->u.remote.client);
|
||||
signer->u.remote.client = NULL;
|
||||
}
|
||||
memset(signer->u.remote.role, 0, sizeof(signer->u.remote.role));
|
||||
}
|
||||
#endif
|
||||
|
||||
free(signer);
|
||||
}
|
||||
|
||||
int nostr_signer_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]) {
|
||||
if (!signer || !out_pubkey_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
return signer_local_get_public_key(signer, out_pubkey_hex);
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
return signer_remote_get_public_key(signer, out_pubkey_hex);
|
||||
}
|
||||
#endif
|
||||
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
int nostr_signer_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out) {
|
||||
if (!signer || !unsigned_event || !signed_event_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
return signer_local_sign_event(signer, unsigned_event, signed_event_out);
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
return signer_remote_sign_event(signer, unsigned_event, signed_event_out);
|
||||
}
|
||||
#endif
|
||||
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
int nostr_signer_nip04_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out) {
|
||||
if (!signer || !peer_pubkey_hex || !plaintext || !ciphertext_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
return signer_local_nip04_encrypt(signer, peer_pubkey_hex, plaintext, ciphertext_out);
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
return signer_remote_nip04_encrypt(signer, peer_pubkey_hex, plaintext, ciphertext_out);
|
||||
}
|
||||
#endif
|
||||
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
int nostr_signer_nip04_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out) {
|
||||
if (!signer || !peer_pubkey_hex || !ciphertext || !plaintext_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
return signer_local_nip04_decrypt(signer, peer_pubkey_hex, ciphertext, plaintext_out);
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
return signer_remote_nip04_decrypt(signer, peer_pubkey_hex, ciphertext, plaintext_out);
|
||||
}
|
||||
#endif
|
||||
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
int nostr_signer_nip44_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out) {
|
||||
if (!signer || !peer_pubkey_hex || !plaintext || !ciphertext_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
return signer_local_nip44_encrypt(signer, peer_pubkey_hex, plaintext, ciphertext_out);
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
return signer_remote_nip44_encrypt(signer, peer_pubkey_hex, plaintext, ciphertext_out);
|
||||
}
|
||||
#endif
|
||||
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
int nostr_signer_nip44_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out) {
|
||||
if (!signer || !peer_pubkey_hex || !ciphertext || !plaintext_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
return signer_local_nip44_decrypt(signer, peer_pubkey_hex, ciphertext, plaintext_out);
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
return signer_remote_nip44_decrypt(signer, peer_pubkey_hex, ciphertext, plaintext_out);
|
||||
}
|
||||
#endif
|
||||
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
int nostr_signer_derive_hmac(nostr_signer_t* signer,
|
||||
const char* data,
|
||||
char out_digest_hex[65]) {
|
||||
if (!signer || !data || !out_digest_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
return signer_local_derive_hmac(signer, data, out_digest_hex);
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
return signer_remote_derive_hmac(signer, data, out_digest_hex);
|
||||
}
|
||||
#endif
|
||||
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
static nostr_signer_t* nostr_signer_nsigner_from_transport(nsigner_transport_t* transport, const char* role) {
|
||||
nsigner_client_t* client = NULL;
|
||||
nostr_signer_t* signer = NULL;
|
||||
|
||||
if (transport == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
client = nsigner_client_new(transport);
|
||||
if (client == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = (nostr_signer_t*)calloc(1, sizeof(*signer));
|
||||
if (signer == NULL) {
|
||||
nsigner_client_free(client);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer->backend = NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE;
|
||||
signer->u.remote.client = client;
|
||||
if (role != NULL) {
|
||||
strncpy(signer->u.remote.role, role, sizeof(signer->u.remote.role) - 1);
|
||||
signer->u.remote.role[sizeof(signer->u.remote.role) - 1] = '\0';
|
||||
}
|
||||
|
||||
return signer;
|
||||
}
|
||||
|
||||
nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role, int timeout_ms) {
|
||||
nsigner_transport_t* transport = NULL;
|
||||
char discovered[8][64];
|
||||
const char* effective_name = socket_name;
|
||||
|
||||
if (effective_name == NULL || effective_name[0] == '\0') {
|
||||
int count = nsigner_transport_list_unix(discovered, 8);
|
||||
if (count != 1) {
|
||||
return NULL;
|
||||
}
|
||||
effective_name = discovered[0];
|
||||
}
|
||||
|
||||
transport = nsigner_transport_open_unix(effective_name, timeout_ms);
|
||||
return nostr_signer_nsigner_from_transport(transport, role);
|
||||
}
|
||||
|
||||
nostr_signer_t* nostr_signer_nsigner_serial(const char* device_path, const char* role, int timeout_ms) {
|
||||
nsigner_transport_t* transport;
|
||||
|
||||
transport = nsigner_transport_open_serial(device_path, timeout_ms);
|
||||
return nostr_signer_nsigner_from_transport(transport, role);
|
||||
}
|
||||
|
||||
nostr_signer_t* nostr_signer_nsigner_tcp(const char* host, int port, const char* role, int timeout_ms) {
|
||||
nsigner_transport_t* transport;
|
||||
|
||||
transport = nsigner_transport_open_tcp(host, port, timeout_ms);
|
||||
return nostr_signer_nsigner_from_transport(transport, role);
|
||||
}
|
||||
|
||||
nostr_signer_t* nostr_signer_nsigner_fds(int read_fd, int write_fd, const char* role, int timeout_ms) {
|
||||
nsigner_transport_t* transport;
|
||||
|
||||
transport = nsigner_transport_open_fds(read_fd, write_fd, timeout_ms);
|
||||
return nostr_signer_nsigner_from_transport(transport, role);
|
||||
}
|
||||
|
||||
int nostr_signer_nsigner_set_auth(nostr_signer_t* signer,
|
||||
const unsigned char auth_privkey[32],
|
||||
const char* label) {
|
||||
if (signer == NULL || auth_privkey == NULL) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend != NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE || signer->u.remote.client == NULL) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
return nsigner_client_set_auth(signer->u.remote.client, auth_privkey, label);
|
||||
}
|
||||
|
||||
nostr_signer_t* nostr_signer_nsigner_qrexec(const char* target_qube, const char* service_name, const char* role, int timeout_ms) {
|
||||
nsigner_transport_t* transport;
|
||||
|
||||
transport = nsigner_transport_open_qrexec(target_qube, service_name, timeout_ms);
|
||||
return nostr_signer_nsigner_from_transport(transport, role);
|
||||
}
|
||||
|
||||
int nostr_signer_nsigner_set_nostr_index(nostr_signer_t* signer, int nostr_index) {
|
||||
if (signer == NULL) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend != NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE || signer->u.remote.client == NULL) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (nostr_index < 0) {
|
||||
/* Clear the nostr_index selector, fall back to role */
|
||||
signer->u.remote.has_nostr_index = 0;
|
||||
signer->u.remote.nostr_index = -1;
|
||||
} else {
|
||||
/* Set nostr_index; clear role to avoid ambiguous selector */
|
||||
signer->u.remote.has_nostr_index = 1;
|
||||
signer->u.remote.nostr_index = nostr_index;
|
||||
memset(signer->u.remote.role, 0, sizeof(signer->u.remote.role));
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
#endif
|
||||
@@ -1,73 +0,0 @@
|
||||
#ifndef NOSTR_SIGNER_H
|
||||
#define NOSTR_SIGNER_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include "nostr_common.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct nostr_signer nostr_signer_t;
|
||||
|
||||
/* Lifecycle */
|
||||
nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]);
|
||||
void nostr_signer_free(nostr_signer_t* signer);
|
||||
|
||||
/* Core verbs */
|
||||
int nostr_signer_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]);
|
||||
int nostr_signer_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out);
|
||||
|
||||
/* Derive a deterministic MAC over caller-supplied data using the signer's
|
||||
* secp256k1 private key: out = HMAC-SHA256(privkey, data).
|
||||
* data is a NUL-terminated UTF-8 string. out_digest_hex receives 64 lowercase
|
||||
* hex chars + NUL. For the local backend this computes directly; for the
|
||||
* nsigner remote backend it calls the "derive" verb (algorithm:"secp256k1").
|
||||
* Returns NOSTR_SUCCESS or an error code. */
|
||||
int nostr_signer_derive_hmac(nostr_signer_t* signer,
|
||||
const char* data,
|
||||
char out_digest_hex[65]);
|
||||
|
||||
/* Encryption verbs (for parity with upcoming remote backends) */
|
||||
int nostr_signer_nip04_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out);
|
||||
int nostr_signer_nip04_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out);
|
||||
int nostr_signer_nip44_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out);
|
||||
int nostr_signer_nip44_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out);
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_serial(const char* device_path, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_tcp(const char* host, int port, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_fds(int read_fd, int write_fd, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_qrexec(const char* target_qube, const char* service_name, const char* role, int timeout_ms);
|
||||
/* TCP mode requires auth envelope per n_signer; set this before making calls. */
|
||||
int nostr_signer_nsigner_set_auth(nostr_signer_t* signer,
|
||||
const unsigned char auth_privkey[32],
|
||||
const char* label);
|
||||
/*
|
||||
* Set the nostr_index selector for the remote nsigner backend. When set,
|
||||
* requests use {"nostr_index":N} instead of {"role":"..."}. This is mutually
|
||||
* exclusive with the role parameter (setting index clears role).
|
||||
* Returns NOSTR_SUCCESS or an error code.
|
||||
*/
|
||||
int nostr_signer_nsigner_set_nostr_index(nostr_signer_t* signer, int nostr_index);
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_SIGNER_H */
|
||||
@@ -1,383 +0,0 @@
|
||||
#define _POSIX_C_SOURCE 200112L
|
||||
|
||||
#include "nsigner_client.h"
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "nip001.h"
|
||||
#include "utils.h"
|
||||
|
||||
/* Ensure a broken connection returns EPIPE instead of killing the process. */
|
||||
static void nsigner_client_ignore_sigpipe_once(void) {
|
||||
static int done = 0;
|
||||
if (!done) {
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_handler = SIG_IGN;
|
||||
(void)sigaction(SIGPIPE, &sa, NULL);
|
||||
done = 1;
|
||||
}
|
||||
}
|
||||
|
||||
struct nsigner_client {
|
||||
nsigner_transport_t* transport;
|
||||
unsigned long long next_id;
|
||||
int auth_enabled;
|
||||
int connected; /* whether the transport has an open connection ready for a request */
|
||||
unsigned char auth_privkey[32];
|
||||
char auth_label[64];
|
||||
char last_error[256];
|
||||
};
|
||||
|
||||
static void nsigner_client_set_error(nsigner_client_t* client, const char* msg) {
|
||||
if (client == NULL) {
|
||||
return;
|
||||
}
|
||||
if (msg == NULL) {
|
||||
msg = "unknown";
|
||||
}
|
||||
strncpy(client->last_error, msg, sizeof(client->last_error) - 1);
|
||||
client->last_error[sizeof(client->last_error) - 1] = '\0';
|
||||
}
|
||||
|
||||
int nsigner_client_compute_body_hash_hex(const cJSON* params, char out_hash_hex[65]) {
|
||||
unsigned char hash[32];
|
||||
char* compact;
|
||||
|
||||
if (out_hash_hex == NULL) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (params == NULL) {
|
||||
compact = (char*)malloc(5);
|
||||
if (compact == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(compact, "null", 5);
|
||||
} else {
|
||||
compact = cJSON_PrintUnformatted((cJSON*)params);
|
||||
if (compact == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
if (nostr_sha256((const unsigned char*)compact, strlen(compact), hash) != 0) {
|
||||
free(compact);
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(hash, sizeof(hash), out_hash_hex);
|
||||
out_hash_hex[64] = '\0';
|
||||
free(compact);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static cJSON* nsigner_client_make_tag_pair(const char* key, const char* value) {
|
||||
cJSON* pair;
|
||||
|
||||
if (key == NULL || value == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pair = cJSON_CreateArray();
|
||||
if (pair == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(pair, cJSON_CreateString(key));
|
||||
cJSON_AddItemToArray(pair, cJSON_CreateString(value));
|
||||
return pair;
|
||||
}
|
||||
|
||||
static int nsigner_client_build_auth(nsigner_client_t* client,
|
||||
const char* request_id,
|
||||
const char* method,
|
||||
const cJSON* params,
|
||||
cJSON** out_auth) {
|
||||
cJSON* tags = NULL;
|
||||
cJSON* auth = NULL;
|
||||
char hash_hex[65];
|
||||
|
||||
if (client == NULL || request_id == NULL || method == NULL || out_auth == NULL) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*out_auth = NULL;
|
||||
|
||||
if (nsigner_client_compute_body_hash_hex(params, hash_hex) != NOSTR_SUCCESS) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
tags = cJSON_CreateArray();
|
||||
if (tags == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(tags, nsigner_client_make_tag_pair("nsigner_rpc", request_id));
|
||||
cJSON_AddItemToArray(tags, nsigner_client_make_tag_pair("nsigner_method", method));
|
||||
cJSON_AddItemToArray(tags, nsigner_client_make_tag_pair("nsigner_body_hash", hash_hex));
|
||||
|
||||
auth = nostr_create_and_sign_event(27235,
|
||||
client->auth_label,
|
||||
tags,
|
||||
client->auth_privkey,
|
||||
time(NULL));
|
||||
if (auth == NULL) {
|
||||
cJSON_Delete(tags);
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
*out_auth = auth;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int nsigner_client_next_id(nsigner_client_t* client, char out_id[32]) {
|
||||
if (client == NULL || out_id == NULL) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
snprintf(out_id, 32, "%llu", client->next_id);
|
||||
client->next_id++;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int nsigner_client_map_rpc_error(int rpc_code, const char* rpc_message) {
|
||||
if (rpc_code == 2010 || rpc_code == 2011 || rpc_code == 2012 || rpc_code == 2013 ||
|
||||
rpc_code == 2014 || rpc_code == 2015 || rpc_code == 2016 || rpc_code == 2017) {
|
||||
return NOSTR_ERROR_NIP46_AUTH_CHALLENGE;
|
||||
}
|
||||
|
||||
if (rpc_message != NULL) {
|
||||
if (strcmp(rpc_message, "approval_denied") == 0 || strcmp(rpc_message, "unauthorized") == 0) {
|
||||
return NOSTR_ERROR_AUTH_RULES_DENIED;
|
||||
}
|
||||
if (strcmp(rpc_message, "method_not_found") == 0) {
|
||||
return NOSTR_ERROR_NIP46_UNKNOWN_METHOD;
|
||||
}
|
||||
if (strcmp(rpc_message, "invalid_request") == 0) {
|
||||
return NOSTR_ERROR_NIP46_INVALID_REQUEST;
|
||||
}
|
||||
if (strcmp(rpc_message, "ambiguous_role_selector") == 0 || strcmp(rpc_message, "unknown_role") == 0) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
}
|
||||
|
||||
if (rpc_code >= 1000 && rpc_code < 2000) {
|
||||
return NOSTR_ERROR_NIP46_INVALID_REQUEST;
|
||||
}
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
nsigner_client_t* nsigner_client_new(nsigner_transport_t* transport) {
|
||||
nsigner_client_t* client;
|
||||
|
||||
if (transport == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
client = (nsigner_client_t*)calloc(1, sizeof(*client));
|
||||
if (client == NULL) {
|
||||
transport->close(transport);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
client->transport = transport;
|
||||
client->next_id = 1;
|
||||
client->connected = 1; /* transport was opened connected by its constructor */
|
||||
nsigner_client_set_error(client, "ok");
|
||||
return client;
|
||||
}
|
||||
|
||||
void nsigner_client_free(nsigner_client_t* client) {
|
||||
if (client == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (client->transport != NULL) {
|
||||
client->transport->close(client->transport);
|
||||
client->transport = NULL;
|
||||
}
|
||||
|
||||
memset(client->auth_privkey, 0, sizeof(client->auth_privkey));
|
||||
memset(client->auth_label, 0, sizeof(client->auth_label));
|
||||
free(client);
|
||||
}
|
||||
|
||||
int nsigner_client_set_auth(nsigner_client_t* client, const unsigned char privkey[32], const char* label) {
|
||||
if (client == NULL || privkey == NULL) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
memcpy(client->auth_privkey, privkey, 32);
|
||||
client->auth_enabled = 1;
|
||||
|
||||
if (label == NULL) {
|
||||
label = "";
|
||||
}
|
||||
|
||||
strncpy(client->auth_label, label, sizeof(client->auth_label) - 1);
|
||||
client->auth_label[sizeof(client->auth_label) - 1] = '\0';
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int nsigner_client_call(nsigner_client_t* client,
|
||||
const char* method,
|
||||
cJSON* params,
|
||||
cJSON** out_result) {
|
||||
cJSON* req = NULL;
|
||||
cJSON* res = NULL;
|
||||
cJSON* result_item;
|
||||
cJSON* error_item;
|
||||
cJSON* code_item;
|
||||
cJSON* message_item;
|
||||
cJSON* params_item;
|
||||
cJSON* auth = NULL;
|
||||
char id_buf[32];
|
||||
char* req_json = NULL;
|
||||
char* res_json = NULL;
|
||||
size_t res_len = 0;
|
||||
int rc;
|
||||
|
||||
if (client == NULL || method == NULL || out_result == NULL || client->transport == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*out_result = NULL;
|
||||
|
||||
nsigner_client_ignore_sigpipe_once();
|
||||
|
||||
/*
|
||||
* The n_signer server closes the connection after handling a single framed
|
||||
* request (one-request-per-connection model). Reconnect before every call
|
||||
* except the first (the transport was opened connected by the constructor).
|
||||
* Transports that do not support reconnection (fd-pair) skip this.
|
||||
*/
|
||||
if (client->connected && client->transport->reconnect != NULL) {
|
||||
if (client->transport->reconnect(client->transport) != 0) {
|
||||
nsigner_client_set_error(client, "transport reconnect failed");
|
||||
cJSON_Delete(params);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
}
|
||||
client->connected = 1;
|
||||
|
||||
rc = nsigner_client_next_id(client, id_buf);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(params);
|
||||
return rc;
|
||||
}
|
||||
|
||||
req = cJSON_CreateObject();
|
||||
if (req == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(req, "id", id_buf);
|
||||
cJSON_AddStringToObject(req, "method", method);
|
||||
|
||||
if (params == NULL) {
|
||||
params_item = cJSON_CreateNull();
|
||||
} else {
|
||||
params_item = params;
|
||||
params = NULL;
|
||||
}
|
||||
|
||||
if (params_item == NULL) {
|
||||
cJSON_Delete(req);
|
||||
cJSON_Delete(params);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
cJSON_AddItemToObject(req, "params", params_item);
|
||||
|
||||
if (client->auth_enabled) {
|
||||
rc = nsigner_client_build_auth(client, id_buf, method, params_item, &auth);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
nsigner_client_set_error(client, "failed to build auth envelope");
|
||||
cJSON_Delete(req);
|
||||
return rc;
|
||||
}
|
||||
cJSON_AddItemToObject(req, "auth", auth);
|
||||
auth = NULL;
|
||||
}
|
||||
|
||||
req_json = cJSON_PrintUnformatted(req);
|
||||
cJSON_Delete(req);
|
||||
if (req_json == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = client->transport->send_framed(client->transport, req_json, strlen(req_json));
|
||||
free(req_json);
|
||||
if (rc != 0) {
|
||||
nsigner_client_set_error(client, "transport send failed");
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
|
||||
rc = client->transport->recv_framed(client->transport, &res_json, &res_len);
|
||||
if (rc != 0 || res_json == NULL || res_len == 0) {
|
||||
free(res_json);
|
||||
nsigner_client_set_error(client, "transport receive failed");
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
|
||||
res = cJSON_Parse(res_json);
|
||||
free(res_json);
|
||||
if (res == NULL) {
|
||||
nsigner_client_set_error(client, "invalid JSON response");
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
error_item = cJSON_GetObjectItemCaseSensitive(res, "error");
|
||||
if (cJSON_IsObject(error_item)) {
|
||||
int rpc_code = 0;
|
||||
const char* rpc_msg = "rpc_error";
|
||||
|
||||
code_item = cJSON_GetObjectItemCaseSensitive(error_item, "code");
|
||||
message_item = cJSON_GetObjectItemCaseSensitive(error_item, "message");
|
||||
if (cJSON_IsNumber(code_item)) {
|
||||
rpc_code = code_item->valueint;
|
||||
}
|
||||
if (cJSON_IsString(message_item) && message_item->valuestring != NULL) {
|
||||
rpc_msg = message_item->valuestring;
|
||||
}
|
||||
|
||||
nsigner_client_set_error(client, rpc_msg);
|
||||
cJSON_Delete(res);
|
||||
return nsigner_client_map_rpc_error(rpc_code, rpc_msg);
|
||||
}
|
||||
|
||||
result_item = cJSON_GetObjectItemCaseSensitive(res, "result");
|
||||
if (result_item == NULL) {
|
||||
nsigner_client_set_error(client, "missing result field");
|
||||
cJSON_Delete(res);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
*out_result = cJSON_Duplicate(result_item, 1);
|
||||
cJSON_Delete(res);
|
||||
|
||||
if (*out_result == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
nsigner_client_set_error(client, "ok");
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
const char* nsigner_client_last_error(const nsigner_client_t* client) {
|
||||
if (client == NULL) {
|
||||
return "invalid_client";
|
||||
}
|
||||
return client->last_error;
|
||||
}
|
||||
|
||||
#endif /* NOSTR_ENABLE_NSIGNER_CLIENT */
|
||||
@@ -1,44 +0,0 @@
|
||||
#ifndef NSIGNER_CLIENT_H
|
||||
#define NSIGNER_CLIENT_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nsigner_transport.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
|
||||
typedef struct nsigner_client nsigner_client_t;
|
||||
|
||||
nsigner_client_t* nsigner_client_new(nsigner_transport_t* transport); /* takes ownership of transport */
|
||||
void nsigner_client_free(nsigner_client_t* client);
|
||||
|
||||
int nsigner_client_set_auth(nsigner_client_t* client, const unsigned char privkey[32], const char* label);
|
||||
|
||||
/*
|
||||
* low-level: build request JSON, attach auth envelope if enabled, send, recv, parse result/error
|
||||
* params may be NULL and ownership is transferred to the call.
|
||||
*/
|
||||
int nsigner_client_call(nsigner_client_t* client,
|
||||
const char* method,
|
||||
cJSON* params,
|
||||
cJSON** out_result);
|
||||
|
||||
const char* nsigner_client_last_error(const nsigner_client_t* client);
|
||||
|
||||
/* Exposed for offline tests and parity checks with n_signer hashing behavior. */
|
||||
int nsigner_client_compute_body_hash_hex(const cJSON* params, char out_hash_hex[65]);
|
||||
|
||||
#endif /* NOSTR_ENABLE_NSIGNER_CLIENT */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NSIGNER_CLIENT_H */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,69 +0,0 @@
|
||||
#ifndef NSIGNER_TRANSPORT_H
|
||||
#define NSIGNER_TRANSPORT_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
|
||||
typedef struct nsigner_transport nsigner_transport_t;
|
||||
|
||||
struct nsigner_transport {
|
||||
int (*send_framed)(nsigner_transport_t* t, const char* json, size_t len);
|
||||
int (*recv_framed)(nsigner_transport_t* t, char** out_json, size_t* out_len); /* caller frees *out_json */
|
||||
/*
|
||||
* Re-establish the underlying connection. The n_signer server closes the
|
||||
* connection after handling a single framed request, so the client must
|
||||
* reconnect before each call. Returns 0 on success, non-zero on failure.
|
||||
* May be NULL for transports that do not support reconnection (e.g. fd-pair);
|
||||
* callers should treat a NULL reconnect as "not supported".
|
||||
*/
|
||||
int (*reconnect)(nsigner_transport_t* t);
|
||||
void (*close)(nsigner_transport_t* t);
|
||||
void* ctx;
|
||||
};
|
||||
|
||||
/* Raw framed I/O helpers for existing connected file descriptors. */
|
||||
int nsigner_transport_send_framed_fd(int fd, const char* json, size_t len);
|
||||
int nsigner_transport_recv_framed_fd(int fd, char** out_json, size_t* out_len);
|
||||
|
||||
/* UNIX abstract socket transport (socket_name without leading '@'). */
|
||||
nsigner_transport_t* nsigner_transport_open_unix(const char* socket_name, int timeout_ms);
|
||||
|
||||
/* TCP transport (host + port), with IPv4/IPv6 resolution via getaddrinfo. */
|
||||
nsigner_transport_t* nsigner_transport_open_tcp(const char* host, int port, int timeout_ms);
|
||||
|
||||
/* USB CDC-ACM serial transport (device path like /dev/ttyACM0). */
|
||||
nsigner_transport_t* nsigner_transport_open_serial(const char* device_path, int timeout_ms);
|
||||
|
||||
/*
|
||||
* Framed transport over existing file descriptors (read_fd, write_fd).
|
||||
* close() closes both owned fds. If passing STDIN_FILENO/STDOUT_FILENO and you do not
|
||||
* want them closed, pass dup()'d descriptors instead.
|
||||
*/
|
||||
nsigner_transport_t* nsigner_transport_open_fds(int read_fd, int write_fd, int timeout_ms);
|
||||
|
||||
/*
|
||||
* Qubes qrexec transport: spawns `qrexec-client-vm <target_qube> <service_name>`
|
||||
* as a subprocess and pipes framed I/O through its stdin/stdout. Each reconnect
|
||||
* re-spawns the subprocess (qrexec handles one request per invocation).
|
||||
* Only works on Qubes OS AppVMs; on other hosts the exec will fail.
|
||||
*/
|
||||
nsigner_transport_t* nsigner_transport_open_qrexec(const char* target_qube, const char* service_name, int timeout_ms);
|
||||
|
||||
/* Enumerate /proc/net/unix abstract sockets beginning with @nsigner, returns count copied. */
|
||||
int nsigner_transport_list_unix(char names[][64], int max_names);
|
||||
|
||||
/* Best-effort enumerate serial devices (e.g. /dev/ttyACM*), returns count copied. */
|
||||
int nsigner_transport_list_serial(char paths[][64], int max_paths);
|
||||
|
||||
#endif /* NOSTR_ENABLE_NSIGNER_CLIENT */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NSIGNER_TRANSPORT_H */
|
||||
+6
-17
@@ -433,38 +433,32 @@ 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
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -1331,11 +1325,6 @@ static int find_word_index(const char* word) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char * const *nostr_bip39_get_wordlist(int *count) {
|
||||
if (count) *count = 2048;
|
||||
return BIP39_WORDLIST;
|
||||
}
|
||||
|
||||
int nostr_bip39_mnemonic_from_bytes(const unsigned char* entropy, size_t entropy_len,
|
||||
char* mnemonic) {
|
||||
if (!entropy || !mnemonic || entropy_len < 16 || entropy_len > 32) return -1;
|
||||
|
||||
@@ -24,11 +24,6 @@ void nostr_bytes_to_hex(const unsigned char *bytes, size_t len, char *hex);
|
||||
// Convert hexadecimal string to bytes
|
||||
int nostr_hex_to_bytes(const char *hex, unsigned char *bytes, size_t len);
|
||||
|
||||
// Get the BIP-39 English wordlist (2048 words). Returns a pointer to a
|
||||
// static array; count is set to 2048 if non-NULL. The pointer is valid
|
||||
// for the lifetime of the process.
|
||||
const char * const *nostr_bip39_get_wordlist(int *count);
|
||||
|
||||
// Base64 encoding function
|
||||
size_t base64_encode(const unsigned char *data, size_t len, char *output,
|
||||
size_t output_size);
|
||||
|
||||
@@ -12,18 +12,6 @@
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <sys/select.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
static int ws_connect_debug_enabled(void) {
|
||||
static int initialized = 0;
|
||||
static int enabled = 0;
|
||||
if (!initialized) {
|
||||
const char* env = getenv("NOSTR_WS_CONNECT_DEBUG");
|
||||
enabled = (env && env[0] != '\0' && strcmp(env, "0") != 0);
|
||||
initialized = 1;
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
|
||||
// OpenSSL headers
|
||||
#include <openssl/ssl.h>
|
||||
@@ -35,7 +23,7 @@ static int ws_connect_debug_enabled(void) {
|
||||
#define WS_MAGIC_STRING "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
#define WS_KEY_LEN 24
|
||||
#define MAX_HEADER_SIZE 4096
|
||||
#define MAX_FRAME_SIZE 262144
|
||||
#define MAX_FRAME_SIZE 65536
|
||||
|
||||
// Transport layer abstraction
|
||||
typedef struct {
|
||||
@@ -57,15 +45,6 @@ typedef struct {
|
||||
SSL_CTX* ssl_ctx;
|
||||
SSL* ssl;
|
||||
int ssl_connected;
|
||||
/* Internal read buffer: SSL_read may return more bytes than the
|
||||
* caller requested (a full TLS record at once). Without buffering
|
||||
* the excess, those bytes are lost — which breaks ws_receive_frame
|
||||
* when it asks for a 2-byte header but SSL_read returns a full
|
||||
* event message. This caused the caching backfill to only receive
|
||||
* 1 event per query from TLS relays. */
|
||||
char read_buf[65536];
|
||||
size_t read_buf_len; /* total bytes in read_buf */
|
||||
size_t read_buf_pos; /* offset of next byte to consume */
|
||||
} tls_transport_t;
|
||||
|
||||
// WebSocket client structure
|
||||
@@ -439,142 +418,32 @@ const char* nostr_ws_strerror(int error_code) {
|
||||
|
||||
static int tcp_connect(void* ctx, const char* host, int port) {
|
||||
tcp_transport_t* tcp = (tcp_transport_t*)ctx;
|
||||
struct addrinfo hints;
|
||||
struct addrinfo* result = NULL;
|
||||
struct addrinfo* rp = NULL;
|
||||
char port_str[16];
|
||||
int ret;
|
||||
int addr_idx = 0;
|
||||
time_t connect_start = time(NULL);
|
||||
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] tcp_connect_start host=%s port=%d\n", host ? host : "(null)", port);
|
||||
}
|
||||
|
||||
tcp->socket_fd = -1;
|
||||
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC; // IPv4 or IPv6
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
hints.ai_protocol = IPPROTO_TCP;
|
||||
|
||||
snprintf(port_str, sizeof(port_str), "%d", port);
|
||||
ret = getaddrinfo(host, port_str, &hints, &result);
|
||||
if (ret != 0 || !result) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] getaddrinfo_failed host=%s port=%d ret=%d err=%s\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
ret,
|
||||
gai_strerror(ret));
|
||||
}
|
||||
|
||||
// Create socket
|
||||
tcp->socket_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (tcp->socket_fd < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (rp = result; rp != NULL; rp = rp->ai_next) {
|
||||
int fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
||||
addr_idx++;
|
||||
if (fd < 0) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] socket_failed host=%s port=%d addr_idx=%d family=%d errno=%d\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
addr_idx,
|
||||
rp->ai_family,
|
||||
errno);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] connect_attempt host=%s port=%d addr_idx=%d family=%d\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
addr_idx,
|
||||
rp->ai_family);
|
||||
}
|
||||
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags >= 0) {
|
||||
(void)fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||
}
|
||||
|
||||
int connect_rc = connect(fd, rp->ai_addr, rp->ai_addrlen);
|
||||
if (connect_rc == 0) {
|
||||
tcp->socket_fd = fd;
|
||||
if (flags >= 0) {
|
||||
(void)fcntl(fd, F_SETFL, flags);
|
||||
}
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] connect_success host=%s port=%d addr_idx=%d elapsed_s=%ld\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
addr_idx,
|
||||
(long)(time(NULL) - connect_start));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (connect_rc < 0 && errno == EINPROGRESS) {
|
||||
fd_set wfds;
|
||||
struct timeval tv;
|
||||
int sel_rc;
|
||||
int so_error = 0;
|
||||
socklen_t so_error_len = sizeof(so_error);
|
||||
|
||||
FD_ZERO(&wfds);
|
||||
FD_SET(fd, &wfds);
|
||||
tv.tv_sec = 5;
|
||||
tv.tv_usec = 0;
|
||||
|
||||
sel_rc = select(fd + 1, NULL, &wfds, NULL, &tv);
|
||||
if (sel_rc > 0 && FD_ISSET(fd, &wfds) &&
|
||||
getsockopt(fd, SOL_SOCKET, SO_ERROR, &so_error, &so_error_len) == 0 &&
|
||||
so_error == 0) {
|
||||
tcp->socket_fd = fd;
|
||||
if (flags >= 0) {
|
||||
(void)fcntl(fd, F_SETFL, flags);
|
||||
}
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] connect_success_after_wait host=%s port=%d addr_idx=%d elapsed_s=%ld\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
addr_idx,
|
||||
(long)(time(NULL) - connect_start));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] connect_timeout_or_error host=%s port=%d addr_idx=%d select_rc=%d so_error=%d elapsed_s=%ld\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
addr_idx,
|
||||
sel_rc,
|
||||
so_error,
|
||||
(long)(time(NULL) - connect_start));
|
||||
}
|
||||
} else if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] connect_failed host=%s port=%d addr_idx=%d errno=%d elapsed_s=%ld\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
addr_idx,
|
||||
errno,
|
||||
(long)(time(NULL) - connect_start));
|
||||
}
|
||||
|
||||
close(fd);
|
||||
|
||||
// Resolve hostname
|
||||
struct hostent* he = gethostbyname(host);
|
||||
if (!he) {
|
||||
close(tcp->socket_fd);
|
||||
tcp->socket_fd = -1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
freeaddrinfo(result);
|
||||
|
||||
if (tcp->socket_fd < 0) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] tcp_connect_end host=%s port=%d rc=-1 elapsed_s=%ld\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
(long)(time(NULL) - connect_start));
|
||||
}
|
||||
|
||||
// Set up address
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length);
|
||||
|
||||
// Connect
|
||||
if (connect(tcp->socket_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
close(tcp->socket_fd);
|
||||
tcp->socket_fd = -1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -584,14 +453,7 @@ static int tcp_connect(void* ctx, const char* host, int port) {
|
||||
io_timeout.tv_usec = 0;
|
||||
(void)setsockopt(tcp->socket_fd, SOL_SOCKET, SO_RCVTIMEO, &io_timeout, sizeof(io_timeout));
|
||||
(void)setsockopt(tcp->socket_fd, SOL_SOCKET, SO_SNDTIMEO, &io_timeout, sizeof(io_timeout));
|
||||
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] tcp_connect_end host=%s port=%d rc=0 elapsed_s=%ld\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
(long)(time(NULL) - connect_start));
|
||||
}
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -717,27 +579,10 @@ static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) {
|
||||
if (!tls->ssl_connected || !tls->ssl) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Serve from the internal read buffer first. SSL_read may have
|
||||
* returned more bytes than the caller requested on a previous call
|
||||
* (a full TLS record), and we buffered the excess. Without this,
|
||||
* ws_receive_frame loses the extra bytes when it asks for a 2-byte
|
||||
* header but SSL_read returns a full event message. */
|
||||
if (tls->read_buf_pos < tls->read_buf_len) {
|
||||
size_t avail = tls->read_buf_len - tls->read_buf_pos;
|
||||
size_t to_copy = (avail < len) ? avail : len;
|
||||
memcpy(data, tls->read_buf + tls->read_buf_pos, to_copy);
|
||||
tls->read_buf_pos += to_copy;
|
||||
if (tls->read_buf_pos >= tls->read_buf_len) {
|
||||
tls->read_buf_len = 0;
|
||||
tls->read_buf_pos = 0;
|
||||
}
|
||||
return (int)to_copy;
|
||||
}
|
||||
|
||||
/* Buffer is empty — refill it with a single SSL_read, then serve
|
||||
* the requested bytes from it. This ensures excess bytes are
|
||||
* preserved for subsequent tls_recv calls. */
|
||||
|
||||
// Always use select() to ensure proper blocking behavior
|
||||
// If SSL has pending data, use zero timeout to return immediately
|
||||
// Otherwise use the full timeout to block until data arrives
|
||||
if (timeout_ms > 0) {
|
||||
fd_set readfds;
|
||||
struct timeval tv;
|
||||
@@ -745,6 +590,7 @@ static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) {
|
||||
FD_ZERO(&readfds);
|
||||
FD_SET(tls->socket_fd, &readfds);
|
||||
|
||||
// If SSL has buffered data, use zero timeout; otherwise use full timeout
|
||||
if (SSL_pending(tls->ssl) > 0) {
|
||||
tv.tv_sec = 0;
|
||||
tv.tv_usec = 0;
|
||||
@@ -766,26 +612,16 @@ static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) {
|
||||
const int max_attempts = 10;
|
||||
|
||||
while (attempts < max_attempts) {
|
||||
/* Read into the internal buffer (up to its capacity), then copy
|
||||
* the requested number of bytes to the caller's output. */
|
||||
int ret = SSL_read(tls->ssl, tls->read_buf, sizeof(tls->read_buf));
|
||||
int ret = SSL_read(tls->ssl, data, len);
|
||||
|
||||
if (ret > 0) {
|
||||
tls->read_buf_len = (size_t)ret;
|
||||
tls->read_buf_pos = 0;
|
||||
size_t to_copy = (tls->read_buf_len < len) ? tls->read_buf_len : len;
|
||||
memcpy(data, tls->read_buf, to_copy);
|
||||
tls->read_buf_pos = to_copy;
|
||||
if (tls->read_buf_pos >= tls->read_buf_len) {
|
||||
tls->read_buf_len = 0;
|
||||
tls->read_buf_pos = 0;
|
||||
}
|
||||
return (int)to_copy;
|
||||
return ret; // Success
|
||||
}
|
||||
|
||||
int err = SSL_get_error(tls->ssl, ret);
|
||||
|
||||
if (err == SSL_ERROR_WANT_READ) {
|
||||
// Check if more data is available on socket
|
||||
fd_set readfds;
|
||||
struct timeval tv = {0, 100000}; // 100ms timeout
|
||||
|
||||
@@ -798,7 +634,7 @@ static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) {
|
||||
}
|
||||
|
||||
attempts++;
|
||||
continue;
|
||||
continue; // Retry the SSL read
|
||||
|
||||
} else if (err == SSL_ERROR_WANT_WRITE) {
|
||||
return -1;
|
||||
@@ -1045,22 +881,13 @@ static int ws_send_frame(nostr_ws_client_t* client, ws_opcode_t opcode, const ch
|
||||
static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char* payload, size_t* payload_len, int timeout_ms) {
|
||||
if (!client || !opcode || !payload || !payload_len) return -1;
|
||||
|
||||
unsigned char header[14]; // Max header size
|
||||
char header[14]; // Max header size
|
||||
size_t header_len = 2; // Minimum header size
|
||||
|
||||
// Read basic header
|
||||
int header_result = client->transport->recv(&client->transport_ctx, header, 2, timeout_ms);
|
||||
|
||||
if (header_result != 2) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr,
|
||||
"[nostr_ws][frame] short_header host=%s port=%d got=%d expected=2 timeout_ms=%d errno=%d\n",
|
||||
client->host ? client->host : "(null)",
|
||||
client->port,
|
||||
header_result,
|
||||
timeout_ms,
|
||||
errno);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1072,28 +899,12 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char
|
||||
// Extended length
|
||||
if (len == 126) {
|
||||
if (client->transport->recv(&client->transport_ctx, header + 2, 2, timeout_ms) != 2) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr,
|
||||
"[nostr_ws][frame] short_ext16 host=%s port=%d timeout_ms=%d errno=%d\n",
|
||||
client->host ? client->host : "(null)",
|
||||
client->port,
|
||||
timeout_ms,
|
||||
errno);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
len = ((uint16_t)header[2] << 8) | (uint8_t)header[3];
|
||||
header_len = 4;
|
||||
} else if (len == 127) {
|
||||
if (client->transport->recv(&client->transport_ctx, header + 2, 8, timeout_ms) != 8) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr,
|
||||
"[nostr_ws][frame] short_ext64 host=%s port=%d timeout_ms=%d errno=%d\n",
|
||||
client->host ? client->host : "(null)",
|
||||
client->port,
|
||||
timeout_ms,
|
||||
errno);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
// For simplicity, we don't support 64-bit lengths
|
||||
@@ -1106,14 +917,6 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char
|
||||
uint32_t mask = 0;
|
||||
if (masked) {
|
||||
if (client->transport->recv(&client->transport_ctx, header + header_len, 4, timeout_ms) != 4) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr,
|
||||
"[nostr_ws][frame] short_mask host=%s port=%d timeout_ms=%d errno=%d\n",
|
||||
client->host ? client->host : "(null)",
|
||||
client->port,
|
||||
timeout_ms,
|
||||
errno);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
mask = ((uint32_t)header[header_len] << 24) |
|
||||
@@ -1127,30 +930,10 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char
|
||||
if (len > *payload_len) {
|
||||
char discard[1024];
|
||||
uint64_t remaining = len;
|
||||
int drain_timeout_ms = timeout_ms < 5000 ? 5000 : timeout_ms;
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr,
|
||||
"[nostr_ws][frame] payload_too_large host=%s port=%d frame_len=%llu buffer_cap=%zu drain_timeout_ms=%d\n",
|
||||
client->host ? client->host : "(null)",
|
||||
client->port,
|
||||
(unsigned long long)len,
|
||||
*payload_len,
|
||||
drain_timeout_ms);
|
||||
}
|
||||
while (remaining > 0) {
|
||||
size_t chunk = remaining > sizeof(discard) ? sizeof(discard) : (size_t)remaining;
|
||||
int got = client->transport->recv(&client->transport_ctx, discard, chunk, drain_timeout_ms);
|
||||
int got = client->transport->recv(&client->transport_ctx, discard, chunk, timeout_ms);
|
||||
if (got <= 0) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr,
|
||||
"[nostr_ws][frame] discard_read_failed host=%s port=%d got=%d drain_timeout_ms=%d errno=%d remaining=%llu\n",
|
||||
client->host ? client->host : "(null)",
|
||||
client->port,
|
||||
got,
|
||||
drain_timeout_ms,
|
||||
errno,
|
||||
(unsigned long long)remaining);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
remaining -= (uint64_t)got;
|
||||
@@ -1166,19 +949,7 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char
|
||||
payload + received,
|
||||
len - received,
|
||||
timeout_ms);
|
||||
if (chunk <= 0) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr,
|
||||
"[nostr_ws][frame] payload_read_failed host=%s port=%d got=%d need_remaining=%zu timeout_ms=%d errno=%d\n",
|
||||
client->host ? client->host : "(null)",
|
||||
client->port,
|
||||
chunk,
|
||||
len - received,
|
||||
timeout_ms,
|
||||
errno);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (chunk <= 0) return -1;
|
||||
received += chunk;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
# Plan: n_signer Verb Name Migration for nostr_core_lib
|
||||
|
||||
## Context
|
||||
|
||||
`n_signer` has completed a verb-name migration (see [`n_signer/plans/legacy_verb_aliases.md`](../../n_signer/plans/legacy_verb_aliases.md)). The legacy verb names are gone from the n_signer wire protocol. This document covers the fixes needed in `nostr_core_lib` — the shared C client library that many other projects vendor.
|
||||
|
||||
**Reference:** the new n_signer API is documented in [`n_signer/README.md`](../../n_signer/README.md) §4 (API).
|
||||
|
||||
## What changed in n_signer
|
||||
|
||||
The Nostr protocol verbs gained a `nostr_` prefix. The algorithm-based verbs (`sign`, `verify`, `encapsulate`, `decapsulate`, `derive_shared_secret`, `get_public_key`) are unchanged. The old alias verbs (`sign_data`, `ssh_sign`, `verify_signature`, `kem_encapsulate`, `kem_decapsulate`, `otp_encrypt`, `otp_decrypt`) are gone.
|
||||
|
||||
| Legacy n_signer verb | New n_signer verb |
|
||||
|----------------------|-------------------|
|
||||
| `sign_event` | `nostr_sign_event` |
|
||||
| `mine_event` | `nostr_mine_event` |
|
||||
| `nip04_encrypt` | `nostr_nip04_encrypt` |
|
||||
| `nip04_decrypt` | `nostr_nip04_decrypt` |
|
||||
| `nip44_encrypt` | `nostr_nip44_encrypt` |
|
||||
| `nip44_decrypt` | `nostr_nip44_decrypt` |
|
||||
| `get_public_key` (role-based) | `nostr_get_public_key` |
|
||||
|
||||
**Important distinction:** `get_public_key` is now **algorithm-based** (takes `algorithm`+`index`). The role-based Nostr pubkey verb is `nostr_get_public_key` (takes `nostr_index`/`role`). Since `nostr_core_lib`'s remote signer always uses role-based selectors (`nostr_index`/`role`), the correct replacement is `nostr_get_public_key`.
|
||||
|
||||
## What does NOT change
|
||||
|
||||
- **NIP-46 standard method names** in [`nostr_core/nip046.c`](../nostr_core/nip046.c) — the strings `"sign_event"`, `"get_public_key"`, `"nip04_encrypt"`, `"nip44_encrypt"`, etc. at lines 111-131 are the **NIP-46 wire protocol** (the standard that relays and other bunkers speak). These must stay as-is. They are NOT n_signer verbs.
|
||||
- **High-level C API function names** in [`nostr_core/nostr_signer.h`](../nostr_core/nostr_signer.h) — `nostr_signer_get_public_key()`, `nostr_signer_sign_event()`, `nostr_signer_nip04_encrypt()`, etc. are the nostr_core_lib's own API surface. These names are fine — they're C function names, not wire verbs. Renaming them would break every consumer for no protocol reason.
|
||||
- **The `nostr_signer_local` backend** — local signing doesn't call n_signer at all; it uses libsecp256k1 directly. No changes needed.
|
||||
|
||||
## Files to change
|
||||
|
||||
### 1. `nostr_core/nostr_signer.c` — the remote signer backend
|
||||
|
||||
This is the only file that calls n_signer verbs via `nsigner_client_call()`. All calls use role-based selectors (`nostr_index`/`role`), so all map to the `nostr_*` verbs.
|
||||
|
||||
| Line | Current verb string | New verb string |
|
||||
|------|---------------------|-----------------|
|
||||
| 343 | `"get_public_key"` | `"nostr_get_public_key"` |
|
||||
| 395 | `"sign_event"` | `"nostr_sign_event"` |
|
||||
| 484 | `"nip04_encrypt"` | `"nostr_nip04_encrypt"` |
|
||||
| 491 | `"nip04_decrypt"` | `"nostr_nip04_decrypt"` |
|
||||
| 498 | `"nip44_encrypt"` | `"nostr_nip44_encrypt"` |
|
||||
| 505 | `"nip44_decrypt"` | `"nostr_nip44_decrypt"` |
|
||||
|
||||
These are simple string-literal replacements. The `signer_remote_params_with_selector()` function (line 289) builds `{"nostr_index": N}` or `{"role": "..."}` selectors — these are still valid for the `nostr_*` verbs, so no selector changes needed.
|
||||
|
||||
**Response format note:** `nostr_get_public_key` returns a plain 64-hex-char string by default (same as the old role-based `get_public_key`). The existing parsing at line 348 (`strlen(result->valuestring) != 64`) continues to work. If the caller ever passes `{"format":"structured"}`, the response would be a JSON object string — but the current code doesn't request structured format, so no change needed.
|
||||
|
||||
### 2. `tests/nsigner_client_test.c` — the mock client test
|
||||
|
||||
| Line | Current verb string | New verb string |
|
||||
|------|---------------------|-----------------|
|
||||
| 296 | `"get_public_key"` | `"nostr_get_public_key"` |
|
||||
|
||||
This test uses a mock transport (fds pair) that returns a canned response — it doesn't actually call n_signer. The verb string just needs to match what the real client would send. Update it for consistency.
|
||||
|
||||
### 3. `nostr_core/NSIGNER_INTEGRATION.md` — integration doc
|
||||
|
||||
This doc references the high-level C API function names (`nostr_signer_get_public_key`, `nostr_signer_sign_event`, etc.) which are NOT changing. However, if it documents the underlying n_signer wire verbs anywhere, those references should be updated to the `nostr_*` names. Scan for any wire-verb documentation and update.
|
||||
|
||||
### 4. `plans/nsigner_integration_plan.md` — historical plan
|
||||
|
||||
This is a historical planning doc. Update any wire-verb references for accuracy, or add a note pointing to this migration plan.
|
||||
|
||||
## Files that do NOT need changes
|
||||
|
||||
- `nostr_core/nip046.c` — NIP-46 standard method names (stay as-is, see above).
|
||||
- `nostr_core/nsigner_client.c` — generic transport/client, doesn't hardcode verb names.
|
||||
- `nostr_core/nsigner_transport.c` — transport layer, no verb names.
|
||||
- `nostr_core/nostr_signer.h` — high-level API declarations (function names stay).
|
||||
- `tests/signer_modules_test.c` — tests the local signer, not n_signer verbs.
|
||||
- `tests/nip01_test.c` — NIP-01 tests, no n_signer verb calls.
|
||||
|
||||
## Verification
|
||||
|
||||
1. `grep -rn '"sign_event"\|"nip04_encrypt"\|"nip04_decrypt"\|"nip44_encrypt"\|"nip44_decrypt"\|"mine_event"\|"sign_data"\|"ssh_sign"\|"verify_signature"\|"kem_encapsulate"\|"kem_decapsulate"\|"otp_encrypt"\|"otp_decrypt"' nostr_core/nostr_signer.c` returns no matches.
|
||||
2. `grep -rn '"get_public_key"' nostr_core/nostr_signer.c` returns no matches (replaced by `nostr_get_public_key`).
|
||||
3. `grep -rn '"sign_event"\|"nip04_encrypt"\|"nip44_encrypt"' nostr_core/nip046.c` still returns matches (NIP-46 standard names — these stay).
|
||||
4. Build clean: `bash build.sh` or the project's build target.
|
||||
5. Tests pass: `./build/nsigner_client_test` (or equivalent).
|
||||
|
||||
## Downstream consumers (inherit the fix)
|
||||
|
||||
Once `nostr_core_lib` is updated, these projects that vendor it will pick up the fix when they update their vendored copy:
|
||||
|
||||
- `sovereign_browser/nostr_core_lib/`
|
||||
- `didactyl/nostr_core_lib/`
|
||||
- `fips/nostr_core_lib/`
|
||||
- `fips_access_point/` (if it vendors nostr_core_lib)
|
||||
- `screen_capture/nostr_core_lib/`
|
||||
- `n_os_tr/includes/nostr_core_lib/` and `n_os_tr/includes/didactyl/nostr_core_lib/`
|
||||
|
||||
Each downstream project should update its vendored `nostr_core/nostr_signer.c` with the same 6 string-literal changes. The `nip046.c` file in each vendored copy should NOT be changed (NIP-46 standard names).
|
||||
|
||||
## Projects with their own n_signer clients (separate fixes)
|
||||
|
||||
These projects have their own n_signer client code (not vendored from nostr_core_lib) and need separate fixes:
|
||||
|
||||
- **`nostr_terminal/src/nsigner_client.c`** — many call sites (lines 1054-1452): `"get_public_key"`→`"nostr_get_public_key"`, `"sign_event"`→`"nostr_sign_event"`, `"nip04_encrypt"`→`"nostr_nip04_encrypt"`, `"nip44_encrypt"`→`"nostr_nip44_encrypt"`. Also vendored into `n_os_tr/includes/nostr_terminal/`.
|
||||
- **`codium_nostr_extension/src/signer/nsignerBackend.ts`** — `"get_public_key"` (line 65)→`"nostr_get_public_key"`, `"sign_event"` (line 76)→`"nostr_sign_event"`.
|
||||
- **`nostr_push/nostr_push`** (bash script) — `"sign_event"` (line 394)→`"nostr_sign_event"`, `"get_public_key"`→`"nostr_get_public_key"`.
|
||||
- **`laantungir_website/scripts/get_nsigner_pubkey.js`** — `"get_public_key"` (line 68)→`"nostr_get_public_key"`.
|
||||
- **`laantungir_website/scripts/publish_nostr.js`** — `"get_public_key"` (line 102)→`"nostr_get_public_key"`, `"sign_event"` (line 112)→`"nostr_sign_event"`.
|
||||
- **`sovereign_browser/src/nostr_bridge.c`** — `"nip04_encrypt"` (line 4525)→`"nostr_nip04_encrypt"`, `"nip44_encrypt"` (line 4529)→`"nostr_nip44_encrypt"`.
|
||||
- **`nostr_login_lite`** — check `src/signers/nsigner-webserial.js`, `nsigner-webusb.js`, and the built `www/` bundle for verb calls.
|
||||
@@ -1,135 +0,0 @@
|
||||
# NIP-03 OpenTimestamps Fix Plan
|
||||
|
||||
## Problem Analysis
|
||||
|
||||
The NIP-03 timestamping implementation in `nostr_core/nip003.c` was never working properly due to several critical bugs in the OTS proof parsing and upgrade logic.
|
||||
|
||||
## OTS Binary Format (Verified against real .ots file)
|
||||
|
||||
### File Structure
|
||||
1. **Header magic** (31 bytes): `\x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94`
|
||||
2. **Major version** (LEB128 varuint): typically `0x01`
|
||||
3. **File hash op** (1 byte): `0x08` = SHA256
|
||||
4. **File digest** (32 bytes for SHA256)
|
||||
5. **Timestamp tree** (recursive structure)
|
||||
|
||||
### Timestamp Tree Format
|
||||
- Read a tag byte
|
||||
- `0xff` = separator (read next tag, process it, continue looping)
|
||||
- `0x00` = attestation follows
|
||||
- Any other byte = op code (process op, then recursively parse sub-timestamp)
|
||||
- The last tag (not preceded by `0xff`) is the final item
|
||||
|
||||
### Attestation Format (after `0x00` tag)
|
||||
- 8-byte attestation tag:
|
||||
- `83dfe30d2ef90c8e` = PendingAttestation
|
||||
- `0588960d73d71901` = BitcoinBlockHeaderAttestation
|
||||
- `06856e0d73d71901` = LitecoinBlockHeaderAttestation
|
||||
- varuint payload length
|
||||
- payload bytes:
|
||||
- Pending: varbytes (URI string like "https://alice.btc.calendar.opentimestamps.org")
|
||||
- Bitcoin: varuint (block height)
|
||||
- Litecoin: varuint (block height)
|
||||
|
||||
### Op Codes
|
||||
- `0x02` = SHA1 (CryptOp, 20-byte digest)
|
||||
- `0x03` = RIPEMD160 (CryptOp, 20-byte digest)
|
||||
- `0x08` = SHA256 (CryptOp, 32-byte digest)
|
||||
- `0x67` = KECCAK256 (CryptOp, 32-byte digest)
|
||||
- `0xf0` = Append (BinaryOp: varbytes argument)
|
||||
- `0xf1` = Prepend (BinaryOp: varbytes argument)
|
||||
- `0xf2` = Reverse (UnaryOp)
|
||||
- `0xf3` = Hexlify (UnaryOp)
|
||||
|
||||
### LEB128 Varuint Format
|
||||
- Read bytes, each contributes 7 bits (low bits)
|
||||
- High bit (0x80) = continue reading
|
||||
- Low bit = final byte
|
||||
|
||||
### Varbytes Format
|
||||
- varuint length prefix
|
||||
- followed by that many bytes
|
||||
|
||||
## Bugs Found
|
||||
|
||||
### Bug 1: Broken `is_proof_complete` (nip003.c:128)
|
||||
```c
|
||||
if (len > 150) {
|
||||
complete = 1;
|
||||
}
|
||||
```
|
||||
Uses a size heuristic instead of parsing the OTS format. A pending proof can be >150 bytes, and a complete proof might be smaller. Should parse the binary format and look for Bitcoin/Litecoin attestation tags.
|
||||
|
||||
### Bug 2: Flawed `upgrade_proof` (nip003.c:184)
|
||||
```c
|
||||
if (resp.body_len <= len) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
Rejects any response that isn't strictly larger. A calendar may return the same proof if nothing changed, or a differently-serialized proof. Should compare content, not just length.
|
||||
|
||||
### Bug 3: CMakeLists.txt missing NIP-03
|
||||
The CMake build only includes a subset of NIPs. `nip003.c` and many other NIP source files are missing from the library build target.
|
||||
|
||||
### Bug 4: No proof verification function
|
||||
No way to verify a proof is cryptographically valid against the Bitcoin blockchain.
|
||||
|
||||
### Bug 5: No signer variant
|
||||
All other NIPs have `_with_signer` variants but NIP-03 only has raw private key version.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: Add OTS parser to nip003.c
|
||||
Add static helper functions:
|
||||
- `ots_read_varuint(data, pos)` - read LEB128 varuint
|
||||
- `ots_read_varbytes(data, pos)` - read varbytes
|
||||
- `ots_parse_timestamp(data, pos, len, results)` - recursively parse timestamp tree
|
||||
- `ots_find_attestations(data, len)` - top-level parser that finds all attestations
|
||||
|
||||
### Step 2: Fix `nostr_nip03_is_proof_complete`
|
||||
Replace the size heuristic with proper parsing:
|
||||
- Parse the OTS file format
|
||||
- Walk the timestamp tree
|
||||
- Return 1 if any Bitcoin (`0588960d73d71901`) or Litecoin (`06856e0d73d71901`) attestation is found
|
||||
- Return 0 if only pending attestations found
|
||||
- Return -1 on parse error
|
||||
|
||||
### Step 3: Fix `nostr_nip03_upgrade_proof`
|
||||
- Remove the `resp.body_len <= len` check
|
||||
- Accept any valid 200 response with non-empty body
|
||||
- Return the base64-encoded response
|
||||
|
||||
### Step 4: Add `nostr_nip03_verify_proof`
|
||||
New function that:
|
||||
- Parses the OTS proof
|
||||
- Extracts Bitcoin attestation(s) with block height(s)
|
||||
- Optionally fetches the Bitcoin block header from a blockchain API
|
||||
- Verifies the merkle root matches the computed digest
|
||||
- Returns the timestamp (block time) on success
|
||||
|
||||
### Step 5: Add `nostr_nip03_create_proof_event_with_signer`
|
||||
Mirror the pattern used by other NIPs for signer support.
|
||||
|
||||
### Step 6: Fix CMakeLists.txt
|
||||
Add all missing NIP source files to the library build target.
|
||||
|
||||
### Step 7: Add example program
|
||||
Create `examples/timestamping.c` demonstrating the full workflow:
|
||||
- Create a Nostr event
|
||||
- Submit to OTS calendar
|
||||
- Poll for completion
|
||||
- Create proof event
|
||||
- Verify proof
|
||||
|
||||
### Step 8: Fix live test
|
||||
Fix the broken curl debug command in `tests/nip03_live_test.c`.
|
||||
|
||||
### Step 9: Add unit tests
|
||||
Add tests for:
|
||||
- OTS parsing with known test vectors
|
||||
- Proof completeness detection (pending vs complete)
|
||||
- Varuint/varbytes encoding/decoding
|
||||
|
||||
### Step 10: Update documentation
|
||||
Update `README.md` and `nostr_core.h` with NIP-03 function documentation.
|
||||
@@ -1,216 +0,0 @@
|
||||
# n_signer Integration into nostr_core_lib — Architecture Plan
|
||||
|
||||
> Status: M1–M6 implemented. This plan remains as architecture history; for current usage/integration contract see:
|
||||
> - `README.md` → "Signer abstraction & nsigner integration"
|
||||
> - `nostr_core/NSIGNER_INTEGRATION.md`
|
||||
> - `plans/n_signer_verb_migration.md` for the completed Nostr wire-method prefix migration
|
||||
>
|
||||
> **Wire-method update:** n_signer's role-based Nostr methods are now
|
||||
> `nostr_get_public_key`, `nostr_sign_event`, `nostr_nip04_encrypt`,
|
||||
> `nostr_nip04_decrypt`, `nostr_nip44_encrypt`, and `nostr_nip44_decrypt`.
|
||||
> The unprefixed `get_public_key` method is algorithm-based and is not used by the
|
||||
> role/`nostr_index` selectors in this library. Public `nostr_signer_*` C API names
|
||||
> and standard NIP-46 method names are unchanged.
|
||||
>
|
||||
> This plan originally described pulling the **caller-side** signer-integration glue out of per-project implementations and into `nostr_core_lib`,
|
||||
> so any project that already links the library can sign **locally**, via a **running
|
||||
> n_signer process**, or via a **USB hardware signer** — with the same code.
|
||||
|
||||
## 1. Problem
|
||||
|
||||
Many projects (`nostr_terminal`, `nostr_push`, `n_os_tr`, `nostr_login_lite`) each
|
||||
re-implement the *same* caller-side integration with n_signer:
|
||||
|
||||
- 4-byte big-endian length-prefixed framing
|
||||
- kind-27235 auth envelope (`nsigner_rpc` / `nsigner_method` / `nsigner_body_hash`)
|
||||
- the JSON-RPC methods (`nostr_get_public_key`, `nostr_sign_event`, `nostr_nip04_*`, `nostr_nip44_*`)
|
||||
- a per-project `signer.h` abstraction wrapping local-vs-remote signing
|
||||
|
||||
Meanwhile, `nostr_core_lib` exposes ~30+ functions that take a raw
|
||||
`const unsigned char* private_key` and sign/encrypt in-process (e.g.
|
||||
[`nostr_create_and_sign_event()`](../nostr_core/nip001.h:16)), with no seam to
|
||||
substitute a remote signer.
|
||||
|
||||
The dependency direction is already **n_signer → nostr_core_lib** (n_signer vendors
|
||||
nostr_core_lib and uses its crypto). So the natural home for the shared, **key-free**,
|
||||
caller-side glue is `nostr_core_lib` itself. This is not recursive: nothing
|
||||
key-bearing and nothing from the signer *program* moves down.
|
||||
|
||||
## 2. What moves vs. what stays
|
||||
|
||||
**Moves into `nostr_core_lib`** (caller-side, key-free, shared):
|
||||
- `nostr_signer_t` abstraction (the backend-agnostic seam).
|
||||
- Local backend (wraps today's raw-privkey path; zero behavior change).
|
||||
- Shared nsigner client: framing + kind-27235 auth envelope + JSON-RPC verbs + discovery.
|
||||
- Caller-side transports: AF_UNIX abstract socket, TCP, stdio, **USB-CDC serial**.
|
||||
- Parallel `*_with_signer` entry points.
|
||||
|
||||
**Stays in `n_signer`** (the program + host/device specifics):
|
||||
- The signer **binary** itself: mnemonic, role derivation, purpose/curve enforcement,
|
||||
attended TUI, approval prompts — the **trust boundary**. Must not move.
|
||||
- Spawn/embed helper (memfd + fexecve + terminal launch) — desktop-Linux deployment.
|
||||
- **Hardware-signer firmware** (ESP32/Feather, TinyUSB) — developing the device.
|
||||
- Browser WebUSB/WebSerial drivers (JS) — stay in `nostr_login_lite`.
|
||||
- Qubes packaging, FIPS, Docker.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph CORE[nostr_core_lib]
|
||||
ABS[nostr_signer_t abstraction]
|
||||
LOCAL[Local backend]
|
||||
CLIENT[nsigner client: framing + kind-27235 auth + discovery]
|
||||
WS[signer-aware *_with_signer entry points]
|
||||
TRANS[Transports: unix / tcp / stdio / usb-cdc serial]
|
||||
end
|
||||
subgraph NS[n_signer]
|
||||
BIN[Signer binary: mnemonic, roles, TUI, approvals]
|
||||
SPAWN[Spawn/embed helper]
|
||||
FW[Hardware firmware ESP32/Feather]
|
||||
end
|
||||
PROJ[Downstream project] --> CORE
|
||||
WS -.wire.-> BIN
|
||||
TRANS -.usb-cdc.-> FW
|
||||
SPAWN -.launches.-> BIN
|
||||
NS --> CORE
|
||||
```
|
||||
|
||||
## 3. Design decisions (locked)
|
||||
|
||||
- **Parallel APIs (Option A).** Add `*_with_signer` variants; leave all existing
|
||||
raw-privkey APIs byte-for-byte untouched. Existing projects keep compiling and
|
||||
behaving identically; remote signing is opt-in per call site.
|
||||
- **Scope = Option 2.** Abstraction + local backend + shared nsigner client (incl.
|
||||
transports) live in `nostr_core_lib`. The spawn/embed helper and the device firmware
|
||||
remain in n_signer.
|
||||
- **USB hardware signer is a caller-side transport.** Desktop C apps reach a USB-CDC
|
||||
hardware signer over `/dev/ttyACM*` using the *same* framing/auth/verbs as the socket
|
||||
transports. The browser WebUSB path and the firmware are out of scope here.
|
||||
- **Build-flag gated.** The remote client + desktop transports compile only when
|
||||
`NOSTR_ENABLE_NSIGNER_CLIENT` is set — ON for desktop, OFF for ESP32 builds and OFF
|
||||
for n_signer's own build. The abstraction + local backend compile everywhere.
|
||||
|
||||
## 4. Module layout (new files)
|
||||
|
||||
- `nostr_core/nostr_signer.{h,c}` — `nostr_signer_t` + vtable + local backend + the
|
||||
six verb entry points operating on a handle.
|
||||
- `nostr_core/nsigner_client.{h,c}` — framing, JSON-RPC verbs, kind-27235 auth envelope
|
||||
builder, error mapping, discovery; remote backend that adapts a connection into a
|
||||
`nostr_signer_t`. (Guarded by `NOSTR_ENABLE_NSIGNER_CLIENT`.)
|
||||
- `nostr_core/nsigner_transport.{h,c}` — pluggable transport vtable
|
||||
(`open / send_framed / recv_framed / close`) with unix-abstract, tcp, stdio, and
|
||||
usb-cdc serial implementations.
|
||||
|
||||
## 5. Signer abstraction (sketch)
|
||||
|
||||
```c
|
||||
typedef struct nostr_signer nostr_signer_t;
|
||||
|
||||
/* Verbs mirror n_signer's wire contract */
|
||||
int nostr_signer_get_public_key(nostr_signer_t*, char out_pubkey_hex[65]);
|
||||
int nostr_signer_sign_event(nostr_signer_t*, cJSON* unsigned_event, cJSON** signed_out);
|
||||
int nostr_signer_nip04_encrypt(nostr_signer_t*, const char* peer_hex, const char* pt, char** ct);
|
||||
int nostr_signer_nip04_decrypt(nostr_signer_t*, const char* peer_hex, const char* ct, char** pt);
|
||||
int nostr_signer_nip44_encrypt(nostr_signer_t*, const char* peer_hex, const char* pt, char** ct);
|
||||
int nostr_signer_nip44_decrypt(nostr_signer_t*, const char* peer_hex, const char* ct, char** pt);
|
||||
void nostr_signer_free(nostr_signer_t*);
|
||||
|
||||
/* Local backend (always available) */
|
||||
nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]);
|
||||
|
||||
/* Remote backends (only with NOSTR_ENABLE_NSIGNER_CLIENT) */
|
||||
nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role,
|
||||
const unsigned char caller_key[32]);
|
||||
nostr_signer_t* nostr_signer_nsigner_tcp(const char* host, int port, const char* role,
|
||||
const unsigned char caller_key[32]);
|
||||
nostr_signer_t* nostr_signer_nsigner_serial(const char* dev_path, const char* role,
|
||||
const unsigned char caller_key[32]);
|
||||
```
|
||||
|
||||
The local backend simply calls the existing in-process crypto; the remote backends
|
||||
serialize each verb into a framed JSON-RPC request, attach the kind-27235 auth envelope,
|
||||
and parse the response.
|
||||
|
||||
## 6. Signer-aware entry points (Phase 3 targets)
|
||||
|
||||
Add parallel functions, highest value first:
|
||||
1. `nostr_create_and_sign_event_with_signer(...)` — the workhorse.
|
||||
2. NIP-17 DM creation (needs nip44 encrypt + sign).
|
||||
3. NIP-46 client session signing.
|
||||
4. NIP-60 / NIP-61 wallet/token/nutzap events.
|
||||
5. blossom auth header creation.
|
||||
6. relay-pool NIP-42 auth (`nostr_relay_pool_set_auth` signer-aware variant).
|
||||
|
||||
Every one keeps its existing raw-privkey sibling unchanged.
|
||||
|
||||
## 7. Phasing
|
||||
|
||||
- **Phase 0** — this doc.
|
||||
- **Phase 1** — `nostr_signer_t` + local backend (portable, ESP32-safe).
|
||||
- **Phase 2** — transport vtable; shared nsigner client (framing/auth/verbs/discovery);
|
||||
unix/tcp/stdio/usb-cdc transports; remote backend.
|
||||
- **Phase 3** — `*_with_signer` entry points (order in §6).
|
||||
- **Phase 4** — build-flag gating; verify n_signer + ESP32 exclude remote pieces;
|
||||
confirm dependency stays n_signer → nostr_core_lib only.
|
||||
- **Phase 5** — example: identical code signing local vs running nsigner (unix) vs USB
|
||||
hardware signer; tests against a live nsigner instance.
|
||||
- **Phase 6** — README/integration docs; cross-link the per-project plans this supersedes.
|
||||
- **Phase 7** (optional) — migrate n_signer's `client/` and the per-project hand-rolled
|
||||
clients onto the shared module; design spawn helper as a clean add-on for later
|
||||
promotion into the core if desired.
|
||||
|
||||
## 8. Risks / open items
|
||||
|
||||
- **Auth envelope canonicalization (JCS).** Must match n_signer exactly
|
||||
(`SHA-256(JCS(params))`, `SHA-256("null")` for no-params). Verify against
|
||||
[`caller_token_identity.md`](../../n_signer/plans/caller_token_identity.md:96).
|
||||
- **Serial transport identity.** USB-CDC is an "asserted caller"; approval prompts on the
|
||||
device still apply. Client must treat `approval_denied` as a normal outcome.
|
||||
- **Memory ownership.** Document free() responsibilities for returned JSON/strings, and
|
||||
zeroization of any caller key buffers.
|
||||
- **Versioning of the wire contract.** Centralizing it here makes nostr_core_lib the
|
||||
single source of truth; n_signer should track it rather than diverge.
|
||||
|
||||
## 9. Driver / acceptance app: `examples/note_poster`
|
||||
|
||||
The work is driven by the simplest possible real Nostr app: **sign in, then post one
|
||||
kind-1 note**. It is the acceptance test for the abstraction — the app is written once
|
||||
and only the signer construction at sign-in changes.
|
||||
|
||||
### Behavior
|
||||
1. Sign in → `nostr_signer_get_public_key()` resolves the npub.
|
||||
2. Prompt for note text → `nostr_create_and_sign_event_with_signer(kind=1, ...)`.
|
||||
3. Publish the signed event to **`wss://relay.laantungir.net`** using the existing relay
|
||||
pool (see [`examples/relay_pool.c`](../examples/relay_pool.c)).
|
||||
4. Print the event id and the relay's OK response.
|
||||
|
||||
### The whole point (one line differs per backend)
|
||||
```c
|
||||
nostr_signer_t *signer;
|
||||
switch (mode) {
|
||||
case LOCAL: signer = nostr_signer_local(privkey); break;
|
||||
case UNIX: signer = nostr_signer_nsigner_unix(socket_name, "main", caller_key); break;
|
||||
case SERIAL: signer = nostr_signer_nsigner_serial("/dev/ttyACM0", "main", caller_key);break;
|
||||
}
|
||||
/* everything below is identical regardless of backend */
|
||||
```
|
||||
|
||||
### Milestones (each independently demoable with the same app)
|
||||
- **M1 — local key, real publish.** `nostr_signer_t` + local backend +
|
||||
`nostr_create_and_sign_event_with_signer`; app posts a kind-1 to
|
||||
`wss://relay.laantungir.net` using a local nsec. No remote/transport/auth code yet.
|
||||
- **M2 — running nsigner over unix socket.** Add framing + kind-27235 auth + unix
|
||||
transport + remote backend. Same app, `--signer unix:<name>`, posts the note via a
|
||||
running `nsigner`.
|
||||
- **M3 — USB hardware signer.** Add the USB-CDC serial transport. Same app,
|
||||
`--signer serial:/dev/ttyACM0`, posts the note via the hardware signer.
|
||||
|
||||
### CLI shape
|
||||
```
|
||||
note_poster --signer local --nsec <hex>
|
||||
note_poster --signer unix:<socket_name> --caller-nsec <hex> [--role main]
|
||||
note_poster --signer serial:/dev/ttyACM0 --caller-nsec <hex> [--role main]
|
||||
```
|
||||
|
||||
### Acceptance criteria
|
||||
A kind-1 note authored by the same code reaches `wss://relay.laantungir.net` and returns
|
||||
a relay OK, across all three signer backends, with only the sign-in line differing.
|
||||
@@ -1,312 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,457 +0,0 @@
|
||||
#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";
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
ref: refs/heads/master
|
||||
@@ -1,7 +0,0 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
||||
[remote "origin"]
|
||||
url = git@git.laantungir.net:laantungir/nostr_core_lib.git
|
||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
||||
@@ -1 +0,0 @@
|
||||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
@@ -1,3 +0,0 @@
|
||||
This file exists to allow you to filter again without --force,
|
||||
and to specify that metadata files should be updated instead
|
||||
of rewritten
|
||||
@@ -1,65 +0,0 @@
|
||||
refs/tags/v0.1.0
|
||||
refs/tags/v0.1.1
|
||||
refs/tags/v0.1.10
|
||||
refs/tags/v0.1.11
|
||||
refs/tags/v0.1.12
|
||||
refs/tags/v0.1.13
|
||||
refs/tags/v0.1.14
|
||||
refs/tags/v0.1.15
|
||||
refs/tags/v0.1.16
|
||||
refs/tags/v0.1.17
|
||||
refs/tags/v0.1.18
|
||||
refs/tags/v0.1.19
|
||||
refs/tags/v0.1.2
|
||||
refs/tags/v0.1.20
|
||||
refs/tags/v0.1.21
|
||||
refs/tags/v0.1.22
|
||||
refs/tags/v0.1.23
|
||||
refs/tags/v0.1.24
|
||||
refs/tags/v0.1.25
|
||||
refs/tags/v0.1.26
|
||||
refs/tags/v0.1.27
|
||||
refs/tags/v0.1.28
|
||||
refs/tags/v0.1.29
|
||||
refs/tags/v0.1.3
|
||||
refs/tags/v0.1.30
|
||||
refs/tags/v0.1.31
|
||||
refs/tags/v0.1.32
|
||||
refs/tags/v0.1.33
|
||||
refs/tags/v0.1.4
|
||||
refs/tags/v0.1.5
|
||||
refs/tags/v0.1.6
|
||||
refs/tags/v0.1.7
|
||||
refs/tags/v0.1.8
|
||||
refs/tags/v0.1.9
|
||||
refs/tags/v0.2.0
|
||||
refs/tags/v0.2.1
|
||||
refs/tags/v0.2.2
|
||||
refs/tags/v0.2.3
|
||||
refs/tags/v0.2.4
|
||||
refs/tags/v0.2.5
|
||||
refs/tags/v0.2.6
|
||||
refs/tags/v0.4.10
|
||||
refs/tags/v0.4.11
|
||||
refs/tags/v0.4.12
|
||||
refs/tags/v0.4.13
|
||||
refs/tags/v0.4.3
|
||||
refs/tags/v0.4.4
|
||||
refs/tags/v0.4.5
|
||||
refs/tags/v0.4.6
|
||||
refs/tags/v0.4.7
|
||||
refs/tags/v0.4.8
|
||||
refs/tags/v0.4.9
|
||||
refs/tags/v0.5.0
|
||||
refs/tags/v0.5.1
|
||||
refs/tags/v0.5.10
|
||||
refs/tags/v0.5.11
|
||||
refs/tags/v0.5.12
|
||||
refs/tags/v0.5.2
|
||||
refs/tags/v0.5.3
|
||||
refs/tags/v0.5.4
|
||||
refs/tags/v0.5.5
|
||||
refs/tags/v0.5.6
|
||||
refs/tags/v0.5.7
|
||||
refs/tags/v0.5.8
|
||||
refs/tags/v0.5.9
|
||||
@@ -1,102 +0,0 @@
|
||||
old new
|
||||
00df0cad99f2689df5bf0470e085a37ea31a6b26 7bf39a7e1071b7c00bb2af90c2c1d4fd911aec45
|
||||
05fe1df8aae540842ee9d86adb269407f65df4aa ec5a97f95139110230214a4636347e4fe71c1671
|
||||
0a8a813fcf3784b4e1da58a7b32cead4ac6699be 84257b2f5865e1963251003c527286125bb549c7
|
||||
0ace93e3030701a1a0085475d626d1bdce11cbe1 f6c8ef62465c110bdf1ebfa383647d5e59432fb6
|
||||
0d910ca18124f06e5840999373723b31796616e8 9bf74e76ce9e8e8c86eaf731d8e36f153972bea1
|
||||
0f897ab1b34b224503843770daf1ae85e2e8c0ff 2224448d12378601af19a0e842e36ea7a10ebfc6
|
||||
13ad24b676e307be79fac71bae7089c66cc65b23 13ad24b676e307be79fac71bae7089c66cc65b23
|
||||
13fd364ca313f475b18e6187ce71406eddf69d42 fcb5abdccfbaca22c2db8caf3074a52d3d7a8b9a
|
||||
19452f45c28484a3fd345037a6e8892ad4392383 9879457d7e1a24a11518ce8a19583d91ecffbeef
|
||||
1a549afa19963831af194c8c755ecd789e1c16dd 81814e8518d5ac9a1060317ec36755187b1cd3ea
|
||||
1da4f6751ea6e275ece6226b13806de630abf482 64aee10a21447f0e839c2d32ead947a6af61b026
|
||||
20006df228cf4320d292960400137303c36c1524 98094cac8612ca4c8cc600fe476f87ad9356c3fd
|
||||
2036d0165b7b1273bdec0631369f18e8a05b127e 3f5024c24c6865ad5eee06566a61912f9c3e6e3b
|
||||
23c2a58c2b0562b2b9b272ecef67f35f748ccd8b 2c128a6295a790e5915b7cc77f7a6716bbdb5e1d
|
||||
23e68bdc83f5ddd49fc5e70b948b6944627f7a97 df50ca1273b47ddf2c5695336118651488225619
|
||||
25bdce73376ee29bdda65959bd929dc74655b4a3 25bdce73376ee29bdda65959bd929dc74655b4a3
|
||||
2637378bf0988323f0a785811baa485da8464e47 2de890da5ccc11255ba8b6e210f098a3566be1a1
|
||||
2fe36acde41e128ec1a297aec0b777d81206b6e1 29d81f9551e6fdc8faf22c5b1b27fbfe26cf9a9b
|
||||
303013a518ac564997707efde79bfb30f428e789 b512be79aba5e10cafad60d8b8acdffa4d4e8b94
|
||||
33129d82fdce8cff280bc0b5ba7ed5e49531606d 2f3140d4f6243b134d64b44d242bc27931fab066
|
||||
392b8632923a624e0bbc6ec3a14c0be088c86538 392b8632923a624e0bbc6ec3a14c0be088c86538
|
||||
39ebfcd90afd4f215db8458e79d0053234389c16 afe3e4787f0e3bbfa493c1cf94372016ab52c9a5
|
||||
3aaa46bb9b98649111ecbbbb042d34707d64e659 cfde3bcb0fb493c6a7cbd4fbfe87ef47b6cceda2
|
||||
3d2537603c56b0035af172aee7a2ccc6f4d9033a e8c0fcdf634f0de320675499486db729661f757b
|
||||
3ebfdc06c09054d950482fb0107be02eedddac0d a9a1aff68248d37cc191939f15a37a31762ddd2b
|
||||
40dd3aa20b1ad45ac9c657d78a342ab02df4efd1 8bcf76387bceb1816bf4c7a57e7fde693b208ee7
|
||||
445ab7a8f42c07167a4f78917cb9228003fd837f 0f72d7433a875a15c163baaea4030f837498b9ad
|
||||
45fb6d061d90fc55bae2b465df94b31b84d53d7f 24903b8c90299066fea6d3e94040d42f9fd2c607
|
||||
481bf3158d041089e7756519cddd387af1e9b536 5c51ab1f9f72ea727e21249a1a8b8321784b85f9
|
||||
499accf44071da5748b27493026f5ffc309fd269 4e06d8f723a07b77a3558a1dd5a768d18bd282cb
|
||||
4d0d794a9a930fdab146dceeb4090305c5218257 6c6bcba650e8805279fffe4427a67051aaa89f9a
|
||||
4dc753cdccc31a5abb8f6e9fab04cf6ff9ce919e 2ce2ecbbfbbbf8ed95e6a2580f8d5dcf2c55e2a7
|
||||
54a6044083ac075c76ea02a90cb308606a50ed09 cdeb569a16940242ac790eb54e17b12ad2f0b519
|
||||
55e2a9c68e449ac375d1bdbb72a2bcf3e6eec9f3 0af77dffab1e6002f3ca990a18a30af7b894ba08
|
||||
58cabadc4466c916923c1002533e2c09583013dc 3533c874eaf1ab0c51f9d27bbd38b387ec1eed1f
|
||||
5a7c79687399d1ace25fbb3036e9ada43b5a5d1f ac85ab6f0799f6a0c209f3efba5feb39d95bafc3
|
||||
5afe802e8fbfa79c3a6204472e3893b4c593a96d c87f8db6d841f9f13dab53981364637b0045ee36
|
||||
6014a250ddebc45c3562f6f7914b221323e5194a 52e5d300fc9552975b95537430d12512a8c6f5c9
|
||||
63972d974d40d0d0e34afcc69f378160e1d83193 e3b92480047469d28c2cd898f68b1a776c8c0ed4
|
||||
64b7c7ec33fb5eb12477a62b3fbc19683aaf3573 57ecbf0c116ac58fec15621a3a98cbd61f5cf1c0
|
||||
654bdd218649d0fb98ef2f74ba681630259e9a20 654bdd218649d0fb98ef2f74ba681630259e9a20
|
||||
669793dd677411ec41bcfb8206574052be4899f0 f07433578a78167a9a96f284331d905d1d395db8
|
||||
693e21423eff56ee6456900cda9812ca7aadb37c 693e21423eff56ee6456900cda9812ca7aadb37c
|
||||
6b95ad37c5cd7f4d128802ead1e5da273d02e65c d7a8aa1dc4ad9021b49cadc1726418e660a0c00d
|
||||
6d7b709f9a047668bb251bb175e9d0d2d9389b4a f86fb256b209ae9c4b5d0a546f93c9f2258bb5a4
|
||||
6ee62e6575d68226512910b301511c448bfe56f7 784cfc09c3a09f93d354801ee3f71245d3704567
|
||||
711a7cc15cbb0b7094e923987cb7e16695b2fc7f 6722d09579b9b5b03fa5ddd892eb3ffdacc4c2b3
|
||||
76e883fad4fab7bce85873ac645dec841358d01d 241ca036ed029e73c99103d5f805d7b76a3f9304
|
||||
76f9d11e069a0ecb58bbc462270e3d150ba28186 d384ade7902b8a9f81c5280ae5bff890afc9fd2f
|
||||
77186c88dd6a440b8fdfac25e91f2342f1ff5f60 553774fdc47734907fc1967c7034fcbf30cb1b79
|
||||
77d92dbcf9f59d08bbf96f9dd42fcdca048b9154 4a096ff35f35c3877839bfd44dcba11ae0a839cf
|
||||
77fadc2683e1ed9cbb8b790420f7201adfdb0f0f 8c610b6e369df848c553edbc882ec4ef5a269bbf
|
||||
7a63f0db57814dc4e467972131ce55b4c4aa0c89 7a63f0db57814dc4e467972131ce55b4c4aa0c89
|
||||
8585e7649c3bd21f21c7bc6f7934269e36b5737c d7e43328bb2ba635a227347db029146aa449fcbe
|
||||
8c5562edb09722075c8c039ec9040030447b9f40 8c5562edb09722075c8c039ec9040030447b9f40
|
||||
8ed9262c6582e2980d643e5312a1ada0029c8be5 ea9b8918f65aa28ac55b0e31557a472ed17d53ba
|
||||
9191d446d3a737883ec0032df30e1ed2d05336c4 a2b700cf40ebce4b9c05d561b2271d04aa1a83f6
|
||||
98a802552b365c2d2a4b81736396d0bb179e9583 9913f92c960d70f7ce4df47e2a7b97a275739fa2
|
||||
98cfd81b01c181f98e00b7ffea7602ff2bfb390d 363b7e9adfc0ead83aba054a86a897b01c0a508a
|
||||
993ca0f2a16c33388efc8c9d0cf4326147c230ea 3cfddbe75b2bc10626bfb6a5092b763a178951b4
|
||||
9a3965243c5d33136c464f867583bbbc5f08304a fca1a8077da44654d025253efc257ea3adc73dfc
|
||||
9a63550863925ba64340240c2c7d89e2f82bc5ff d52ac9c10695bbae7960000512fad6df8cee8762
|
||||
9fd4c61df78be7ee8a92b940650cb8c195430ced dad1f5aa5260cdb7f395f88851f5df158b3cb6fe
|
||||
a3a68f0fde9aaf8a3fadee4a250d4abe94202400 852ff337e57b311c072e3bccf0f28799489d2232
|
||||
a3f7a1bbfd22d0a16ed5fcd0b3c54d79612bcb35 2b80abc3ae6b3060e5ea06316529df88e9ed2276
|
||||
a595747affcbc5a20b476c76c414b8f2dbc59c1e c81db98e3f25c56a808b6c7b3b4df6f4355966dd
|
||||
a8dc2ed0464f4220ef306fa1ce07673dd4e94880 0b2cca65490eef2c43ed7bd5bb6534c18de488c8
|
||||
adc7aa9dcfd16e41eb38c9883d64cfc11dd374bc 79d98587d3d25f4f8778b97e8db9244195c729a8
|
||||
ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e
|
||||
ae672b7047ffdda81bbd8513900e8c80d0996fa7 7aea4b07df314bbf43a4f55b12c764b5141982e2
|
||||
af2117b57474c0757c7bc4c448b5053477019d81 01d382c0d3f08b824e5283c246e98457ca2e6737
|
||||
b3110218fc0c9118fd335d7ff481bc35ad1beaa4 b3110218fc0c9118fd335d7ff481bc35ad1beaa4
|
||||
ba97b1a6e38514713c0ec790f3170f79337a4d8c ba97b1a6e38514713c0ec790f3170f79337a4d8c
|
||||
c0784fc890744e31816cd4a208130015f8302d3e c6896eebb4047d5fe5c61d589d373fff211d8284
|
||||
c0d095e57b11c3065fe4acc9c21245cfec1e6a28 31947d12b735396e2c202f8dced51736ffe80106
|
||||
c109c93382a07eed72c72026e07b9ca19b423f1e bb673cc1a479e0d2a8f7bfef361b1c0bf3fade81
|
||||
c3a948288207572bfb889ccdd21be0f740e12d62 997c3e976badbb7a4aad2561756e8f2fd1bd1940
|
||||
c569c0c34649cf3390e35e8ed0459efc2ed09f74 04f22e6f47d7ac161d88b0a6a440523d4cbfa788
|
||||
ca6b4754f9c57aba7788a55d83eedbe4127943ed 6369fb0cf3e7df34851913534a265878ac1e9828
|
||||
cb4ff939060ea67c06d70435f5e6a30745366256 92d5927392b6eaf8ca5a3aaa611a10ef946f71e8
|
||||
d257ae49f1062dc85fe4850209d0690d925327a8 a4ae8df66fe53dd6906cddbdd136e98a282faf0a
|
||||
d6a0bd67b2a68fbfa586e8022d7dd3f2ceb1a1f3 7cf7ffe025adf43ee8ed90e48fdd7c080bd9f4b4
|
||||
d8b342ca3fd2547e5a14ae75b9b17602e9342b02 a4fbba027e6a6577a62e413e9078be0ae39fc2d0
|
||||
d8d3f9b28c0a8ae07b036a762f8cf00ec65f9860 3186c2ead6bba56da5fc8addf4851c2eaab664e4
|
||||
df23fd618a157804129308b4f872b227a6225caf fdc7cc2a383abbde2a26c1d5cf30b7a803de1637
|
||||
e115d2e03f5d28b993c64da1663933c4e4cde129 e115d2e03f5d28b993c64da1663933c4e4cde129
|
||||
e137560d6443b564ffab42c1ed0c7dbc758e3065 3b076ea372bcd24f94e848faba01e1d50535ce3a
|
||||
e21c8eb5be4bd7d0b0c90c1d3f9fe3c9bfd7396f d264d9f5ed96641abacb2435b10e56b0667b3f6b
|
||||
e40f3037d3f7547d8da5b35e3ff9d2ff24553806 9e9fcba96cd38aca50d67e6a74272f14f13e48c1
|
||||
e77973aea06d2d28de621f828ca5d098eb16e752 c871c402197880af9cf6fcf9b70512d4effef405
|
||||
ea40aec6b867168625edaaad2e1aece5d2069293 af4f7cace95206873830de6d4cf72d59bb2d3b61
|
||||
ea84795911d70c10a635eae63b102b6ad83ed8b9 2b3113dafc9cfe4104a9fdfbf873b69d89ec2848
|
||||
eb7a9e6098aa684f3af13ae8f11e175e080d41a0 e087a17eb927a754da1d70fc48d36dd8f32f8429
|
||||
ec8eb5555b5679dc15cb867f9623c8909d97b153 b0a2279bcd59ddeef80bec22817ce61af5a7d099
|
||||
ef8e68b952ca9d3499e37d9607fc4e44dac8a369 ef8e68b952ca9d3499e37d9607fc4e44dac8a369
|
||||
f1c22bdf530afae20fc258ad990b430543cb6895 dfe8a20e2e97feb67a7506e22c0c3ba766f22f2c
|
||||
f3068f82f3ba5f9f8680af33a1081a9d6e92810d e87d7364ff2d5d8011d77588532fbf653d09891d
|
||||
f470759b960f4bcf06e7e328c564786daa479ae0 4c09cf2b266682253341206978b18d379c77cf87
|
||||
f50384962d4bf357b83335588b881ac74dacae3b ace0b5a7927f99e701c28b0a5d5623ac139cb7dc
|
||||
f7e179dd85e51ec4f4fe801d1280f73f84643cf8 5760a3a7d0660cd58aa8e99919a71e187da4f785
|
||||
fb3c9dbc76754730b0de6b07b721b76f745b4c4b 2a861298da59c45da7e7628b53acc0bb8278582e
|
||||
@@ -1 +0,0 @@
|
||||
ca6b4754f9c57aba7788a55d83eedbe4127943ed 6369fb0cf3e7df34851913534a265878ac1e9828
|
||||
@@ -1,76 +0,0 @@
|
||||
old new ref
|
||||
e115d2e03f5d28b993c64da1663933c4e4cde129 e115d2e03f5d28b993c64da1663933c4e4cde129 refs/heads/master
|
||||
ca6b4754f9c57aba7788a55d83eedbe4127943ed 6369fb0cf3e7df34851913534a265878ac1e9828 refs/tags/v0.1.0
|
||||
ca6b4754f9c57aba7788a55d83eedbe4127943ed 6369fb0cf3e7df34851913534a265878ac1e9828 refs/tags/v0.1.1
|
||||
ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.10
|
||||
ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.11
|
||||
ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.12
|
||||
ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.13
|
||||
ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.14
|
||||
ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.15
|
||||
ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.16
|
||||
ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.17
|
||||
3aaa46bb9b98649111ecbbbb042d34707d64e659 cfde3bcb0fb493c6a7cbd4fbfe87ef47b6cceda2 refs/tags/v0.1.18
|
||||
5a7c79687399d1ace25fbb3036e9ada43b5a5d1f ac85ab6f0799f6a0c209f3efba5feb39d95bafc3 refs/tags/v0.1.19
|
||||
ca6b4754f9c57aba7788a55d83eedbe4127943ed 6369fb0cf3e7df34851913534a265878ac1e9828 refs/tags/v0.1.2
|
||||
5a7c79687399d1ace25fbb3036e9ada43b5a5d1f ac85ab6f0799f6a0c209f3efba5feb39d95bafc3 refs/tags/v0.1.20
|
||||
5a7c79687399d1ace25fbb3036e9ada43b5a5d1f ac85ab6f0799f6a0c209f3efba5feb39d95bafc3 refs/tags/v0.1.21
|
||||
c109c93382a07eed72c72026e07b9ca19b423f1e bb673cc1a479e0d2a8f7bfef361b1c0bf3fade81 refs/tags/v0.1.22
|
||||
c109c93382a07eed72c72026e07b9ca19b423f1e bb673cc1a479e0d2a8f7bfef361b1c0bf3fade81 refs/tags/v0.1.23
|
||||
2637378bf0988323f0a785811baa485da8464e47 2de890da5ccc11255ba8b6e210f098a3566be1a1 refs/tags/v0.1.24
|
||||
77fadc2683e1ed9cbb8b790420f7201adfdb0f0f 8c610b6e369df848c553edbc882ec4ef5a269bbf refs/tags/v0.1.25
|
||||
f1c22bdf530afae20fc258ad990b430543cb6895 dfe8a20e2e97feb67a7506e22c0c3ba766f22f2c refs/tags/v0.1.26
|
||||
9191d446d3a737883ec0032df30e1ed2d05336c4 a2b700cf40ebce4b9c05d561b2271d04aa1a83f6 refs/tags/v0.1.27
|
||||
d6a0bd67b2a68fbfa586e8022d7dd3f2ceb1a1f3 7cf7ffe025adf43ee8ed90e48fdd7c080bd9f4b4 refs/tags/v0.1.28
|
||||
d6a0bd67b2a68fbfa586e8022d7dd3f2ceb1a1f3 7cf7ffe025adf43ee8ed90e48fdd7c080bd9f4b4 refs/tags/v0.1.29
|
||||
ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.3
|
||||
3d2537603c56b0035af172aee7a2ccc6f4d9033a e8c0fcdf634f0de320675499486db729661f757b refs/tags/v0.1.30
|
||||
3d2537603c56b0035af172aee7a2ccc6f4d9033a e8c0fcdf634f0de320675499486db729661f757b refs/tags/v0.1.31
|
||||
c569c0c34649cf3390e35e8ed0459efc2ed09f74 04f22e6f47d7ac161d88b0a6a440523d4cbfa788 refs/tags/v0.1.32
|
||||
f50384962d4bf357b83335588b881ac74dacae3b ace0b5a7927f99e701c28b0a5d5623ac139cb7dc refs/tags/v0.1.33
|
||||
ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.4
|
||||
ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.5
|
||||
ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.6
|
||||
ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.7
|
||||
ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.8
|
||||
ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.9
|
||||
1da4f6751ea6e275ece6226b13806de630abf482 64aee10a21447f0e839c2d32ead947a6af61b026 refs/tags/v0.2.0
|
||||
77d92dbcf9f59d08bbf96f9dd42fcdca048b9154 4a096ff35f35c3877839bfd44dcba11ae0a839cf refs/tags/v0.2.1
|
||||
e40f3037d3f7547d8da5b35e3ff9d2ff24553806 9e9fcba96cd38aca50d67e6a74272f14f13e48c1 refs/tags/v0.2.2
|
||||
c0d095e57b11c3065fe4acc9c21245cfec1e6a28 31947d12b735396e2c202f8dced51736ffe80106 refs/tags/v0.2.3
|
||||
445ab7a8f42c07167a4f78917cb9228003fd837f 0f72d7433a875a15c163baaea4030f837498b9ad refs/tags/v0.2.4
|
||||
9a63550863925ba64340240c2c7d89e2f82bc5ff d52ac9c10695bbae7960000512fad6df8cee8762 refs/tags/v0.2.5
|
||||
0d910ca18124f06e5840999373723b31796616e8 9bf74e76ce9e8e8c86eaf731d8e36f153972bea1 refs/tags/v0.2.6
|
||||
23e68bdc83f5ddd49fc5e70b948b6944627f7a97 df50ca1273b47ddf2c5695336118651488225619 refs/tags/v0.4.10
|
||||
481bf3158d041089e7756519cddd387af1e9b536 5c51ab1f9f72ea727e21249a1a8b8321784b85f9 refs/tags/v0.4.11
|
||||
1a549afa19963831af194c8c755ecd789e1c16dd 81814e8518d5ac9a1060317ec36755187b1cd3ea refs/tags/v0.4.12
|
||||
adc7aa9dcfd16e41eb38c9883d64cfc11dd374bc 79d98587d3d25f4f8778b97e8db9244195c729a8 refs/tags/v0.4.13
|
||||
54a6044083ac075c76ea02a90cb308606a50ed09 cdeb569a16940242ac790eb54e17b12ad2f0b519 refs/tags/v0.4.3
|
||||
6b95ad37c5cd7f4d128802ead1e5da273d02e65c d7a8aa1dc4ad9021b49cadc1726418e660a0c00d refs/tags/v0.4.4
|
||||
499accf44071da5748b27493026f5ffc309fd269 4e06d8f723a07b77a3558a1dd5a768d18bd282cb refs/tags/v0.4.5
|
||||
c0784fc890744e31816cd4a208130015f8302d3e c6896eebb4047d5fe5c61d589d373fff211d8284 refs/tags/v0.4.6
|
||||
23c2a58c2b0562b2b9b272ecef67f35f748ccd8b 2c128a6295a790e5915b7cc77f7a6716bbdb5e1d refs/tags/v0.4.7
|
||||
f3068f82f3ba5f9f8680af33a1081a9d6e92810d e87d7364ff2d5d8011d77588532fbf653d09891d refs/tags/v0.4.8
|
||||
76f9d11e069a0ecb58bbc462270e3d150ba28186 d384ade7902b8a9f81c5280ae5bff890afc9fd2f refs/tags/v0.4.9
|
||||
98cfd81b01c181f98e00b7ffea7602ff2bfb390d 363b7e9adfc0ead83aba054a86a897b01c0a508a refs/tags/v0.5.0
|
||||
ea40aec6b867168625edaaad2e1aece5d2069293 af4f7cace95206873830de6d4cf72d59bb2d3b61 refs/tags/v0.5.1
|
||||
fb3c9dbc76754730b0de6b07b721b76f745b4c4b 2a861298da59c45da7e7628b53acc0bb8278582e refs/tags/v0.5.10
|
||||
f470759b960f4bcf06e7e328c564786daa479ae0 4c09cf2b266682253341206978b18d379c77cf87 refs/tags/v0.5.11
|
||||
ec8eb5555b5679dc15cb867f9623c8909d97b153 b0a2279bcd59ddeef80bec22817ce61af5a7d099 refs/tags/v0.5.12
|
||||
b3110218fc0c9118fd335d7ff481bc35ad1beaa4 b3110218fc0c9118fd335d7ff481bc35ad1beaa4 refs/tags/v0.5.13
|
||||
25bdce73376ee29bdda65959bd929dc74655b4a3 25bdce73376ee29bdda65959bd929dc74655b4a3 refs/tags/v0.5.14
|
||||
ef8e68b952ca9d3499e37d9607fc4e44dac8a369 ef8e68b952ca9d3499e37d9607fc4e44dac8a369 refs/tags/v0.5.15
|
||||
8c5562edb09722075c8c039ec9040030447b9f40 8c5562edb09722075c8c039ec9040030447b9f40 refs/tags/v0.5.16
|
||||
0a8a813fcf3784b4e1da58a7b32cead4ac6699be 84257b2f5865e1963251003c527286125bb549c7 refs/tags/v0.5.2
|
||||
ae672b7047ffdda81bbd8513900e8c80d0996fa7 7aea4b07df314bbf43a4f55b12c764b5141982e2 refs/tags/v0.5.3
|
||||
4d0d794a9a930fdab146dceeb4090305c5218257 6c6bcba650e8805279fffe4427a67051aaa89f9a refs/tags/v0.5.4
|
||||
39ebfcd90afd4f215db8458e79d0053234389c16 afe3e4787f0e3bbfa493c1cf94372016ab52c9a5 refs/tags/v0.5.5
|
||||
a595747affcbc5a20b476c76c414b8f2dbc59c1e c81db98e3f25c56a808b6c7b3b4df6f4355966dd refs/tags/v0.5.6
|
||||
a3f7a1bbfd22d0a16ed5fcd0b3c54d79612bcb35 2b80abc3ae6b3060e5ea06316529df88e9ed2276 refs/tags/v0.5.7
|
||||
f7e179dd85e51ec4f4fe801d1280f73f84643cf8 5760a3a7d0660cd58aa8e99919a71e187da4f785 refs/tags/v0.5.8
|
||||
303013a518ac564997707efde79bfb30f428e789 b512be79aba5e10cafad60d8b8acdffa4d4e8b94 refs/tags/v0.5.9
|
||||
392b8632923a624e0bbc6ec3a14c0be088c86538 392b8632923a624e0bbc6ec3a14c0be088c86538 refs/tags/v0.6.0
|
||||
654bdd218649d0fb98ef2f74ba681630259e9a20 654bdd218649d0fb98ef2f74ba681630259e9a20 refs/tags/v0.6.1
|
||||
693e21423eff56ee6456900cda9812ca7aadb37c 693e21423eff56ee6456900cda9812ca7aadb37c refs/tags/v0.6.2
|
||||
ba97b1a6e38514713c0ec790f3170f79337a4d8c ba97b1a6e38514713c0ec790f3170f79337a4d8c refs/tags/v0.6.3
|
||||
7a63f0db57814dc4e467972131ce55b4c4aa0c89 7a63f0db57814dc4e467972131ce55b4c4aa0c89 refs/tags/v0.6.4
|
||||
@@ -1 +0,0 @@
|
||||
No filtering problems encountered.
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message taken by
|
||||
# applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit. The hook is
|
||||
# allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "applypatch-msg".
|
||||
|
||||
. git-sh-setup
|
||||
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
|
||||
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
|
||||
:
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message.
|
||||
# Called by "git commit" with one argument, the name of the file
|
||||
# that has the commit message. The hook should exit with non-zero
|
||||
# status after issuing an appropriate message if it wants to stop the
|
||||
# commit. The hook is allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "commit-msg".
|
||||
|
||||
# Uncomment the below to add a Signed-off-by line to the message.
|
||||
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
|
||||
# hook is more suited to it.
|
||||
#
|
||||
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
||||
|
||||
# This example catches duplicate Signed-off-by lines.
|
||||
|
||||
test "" = "$(grep '^Signed-off-by: ' "$1" |
|
||||
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
|
||||
echo >&2 Duplicate Signed-off-by lines.
|
||||
exit 1
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use IPC::Open2;
|
||||
|
||||
# An example hook script to integrate Watchman
|
||||
# (https://facebook.github.io/watchman/) with git to speed up detecting
|
||||
# new and modified files.
|
||||
#
|
||||
# The hook is passed a version (currently 2) and last update token
|
||||
# formatted as a string and outputs to stdout a new update token and
|
||||
# all files that have been modified since the update token. Paths must
|
||||
# be relative to the root of the working tree and separated by a single NUL.
|
||||
#
|
||||
# To enable this hook, rename this file to "query-watchman" and set
|
||||
# 'git config core.fsmonitor .git/hooks/query-watchman'
|
||||
#
|
||||
my ($version, $last_update_token) = @ARGV;
|
||||
|
||||
# Uncomment for debugging
|
||||
# print STDERR "$0 $version $last_update_token\n";
|
||||
|
||||
# Check the hook interface version
|
||||
if ($version ne 2) {
|
||||
die "Unsupported query-fsmonitor hook version '$version'.\n" .
|
||||
"Falling back to scanning...\n";
|
||||
}
|
||||
|
||||
my $git_work_tree = get_working_dir();
|
||||
|
||||
my $retry = 1;
|
||||
|
||||
my $json_pkg;
|
||||
eval {
|
||||
require JSON::XS;
|
||||
$json_pkg = "JSON::XS";
|
||||
1;
|
||||
} or do {
|
||||
require JSON::PP;
|
||||
$json_pkg = "JSON::PP";
|
||||
};
|
||||
|
||||
launch_watchman();
|
||||
|
||||
sub launch_watchman {
|
||||
my $o = watchman_query();
|
||||
if (is_work_tree_watched($o)) {
|
||||
output_result($o->{clock}, @{$o->{files}});
|
||||
}
|
||||
}
|
||||
|
||||
sub output_result {
|
||||
my ($clockid, @files) = @_;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# binmode $fh, ":utf8";
|
||||
# print $fh "$clockid\n@files\n";
|
||||
# close $fh;
|
||||
|
||||
binmode STDOUT, ":utf8";
|
||||
print $clockid;
|
||||
print "\0";
|
||||
local $, = "\0";
|
||||
print @files;
|
||||
}
|
||||
|
||||
sub watchman_clock {
|
||||
my $response = qx/watchman clock "$git_work_tree"/;
|
||||
die "Failed to get clock id on '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub watchman_query {
|
||||
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
|
||||
or die "open2() failed: $!\n" .
|
||||
"Falling back to scanning...\n";
|
||||
|
||||
# In the query expression below we're asking for names of files that
|
||||
# changed since $last_update_token but not from the .git folder.
|
||||
#
|
||||
# To accomplish this, we're using the "since" generator to use the
|
||||
# recency index to select candidate nodes and "fields" to limit the
|
||||
# output to file names only. Then we're using the "expression" term to
|
||||
# further constrain the results.
|
||||
my $last_update_line = "";
|
||||
if (substr($last_update_token, 0, 1) eq "c") {
|
||||
$last_update_token = "\"$last_update_token\"";
|
||||
$last_update_line = qq[\n"since": $last_update_token,];
|
||||
}
|
||||
my $query = <<" END";
|
||||
["query", "$git_work_tree", {$last_update_line
|
||||
"fields": ["name"],
|
||||
"expression": ["not", ["dirname", ".git"]]
|
||||
}]
|
||||
END
|
||||
|
||||
# Uncomment for debugging the watchman query
|
||||
# open (my $fh, ">", ".git/watchman-query.json");
|
||||
# print $fh $query;
|
||||
# close $fh;
|
||||
|
||||
print CHLD_IN $query;
|
||||
close CHLD_IN;
|
||||
my $response = do {local $/; <CHLD_OUT>};
|
||||
|
||||
# Uncomment for debugging the watch response
|
||||
# open ($fh, ">", ".git/watchman-response.json");
|
||||
# print $fh $response;
|
||||
# close $fh;
|
||||
|
||||
die "Watchman: command returned no output.\n" .
|
||||
"Falling back to scanning...\n" if $response eq "";
|
||||
die "Watchman: command returned invalid output: $response\n" .
|
||||
"Falling back to scanning...\n" unless $response =~ /^\{/;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub is_work_tree_watched {
|
||||
my ($output) = @_;
|
||||
my $error = $output->{error};
|
||||
if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
|
||||
$retry--;
|
||||
my $response = qx/watchman watch "$git_work_tree"/;
|
||||
die "Failed to make watchman watch '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
$output = $json_pkg->new->utf8->decode($response);
|
||||
$error = $output->{error};
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# close $fh;
|
||||
|
||||
# Watchman will always return all files on the first query so
|
||||
# return the fast "everything is dirty" flag to git and do the
|
||||
# Watchman query just to get it over with now so we won't pay
|
||||
# the cost in git to look up each individual file.
|
||||
my $o = watchman_clock();
|
||||
$error = $output->{error};
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
output_result($o->{clock}, ("/"));
|
||||
$last_update_token = $o->{clock};
|
||||
|
||||
eval { launch_watchman() };
|
||||
return 0;
|
||||
}
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub get_working_dir {
|
||||
my $working_dir;
|
||||
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
|
||||
$working_dir = Win32::GetCwd();
|
||||
$working_dir =~ tr/\\/\//;
|
||||
} else {
|
||||
require Cwd;
|
||||
$working_dir = Cwd::cwd();
|
||||
}
|
||||
|
||||
return $working_dir;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare a packed repository for use over
|
||||
# dumb transports.
|
||||
#
|
||||
# To enable this hook, rename this file to "post-update".
|
||||
|
||||
exec git update-server-info
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed
|
||||
# by applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-applypatch".
|
||||
|
||||
. git-sh-setup
|
||||
precommit="$(git rev-parse --git-path hooks/pre-commit)"
|
||||
test -x "$precommit" && exec "$precommit" ${1+"$@"}
|
||||
:
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git commit" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message if
|
||||
# it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-commit".
|
||||
|
||||
if git rev-parse --verify HEAD >/dev/null 2>&1
|
||||
then
|
||||
against=HEAD
|
||||
else
|
||||
# Initial commit: diff against an empty tree object
|
||||
against=$(git hash-object -t tree /dev/null)
|
||||
fi
|
||||
|
||||
# If you want to allow non-ASCII filenames set this variable to true.
|
||||
allownonascii=$(git config --type=bool hooks.allownonascii)
|
||||
|
||||
# Redirect output to stderr.
|
||||
exec 1>&2
|
||||
|
||||
# Cross platform projects tend to avoid non-ASCII filenames; prevent
|
||||
# them from being added to the repository. We exploit the fact that the
|
||||
# printable range starts at the space character and ends with tilde.
|
||||
if [ "$allownonascii" != "true" ] &&
|
||||
# Note that the use of brackets around a tr range is ok here, (it's
|
||||
# even required, for portability to Solaris 10's /usr/bin/tr), since
|
||||
# the square bracket bytes happen to fall in the designated range.
|
||||
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
|
||||
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
|
||||
then
|
||||
cat <<\EOF
|
||||
Error: Attempt to add a non-ASCII file name.
|
||||
|
||||
This can cause problems if you want to work with people on other platforms.
|
||||
|
||||
To be portable it is advisable to rename the file.
|
||||
|
||||
If you know what you are doing you can disable this check using:
|
||||
|
||||
git config hooks.allownonascii true
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If there are whitespace errors, print the offending file names and fail.
|
||||
exec git diff-index --check --cached $against --
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git merge" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message to
|
||||
# stderr if it wants to stop the merge commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-merge-commit".
|
||||
|
||||
. git-sh-setup
|
||||
test -x "$GIT_DIR/hooks/pre-commit" &&
|
||||
exec "$GIT_DIR/hooks/pre-commit"
|
||||
:
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# An example hook script to verify what is about to be pushed. Called by "git
|
||||
# push" after it has checked the remote status, but before anything has been
|
||||
# pushed. If this script exits with a non-zero status nothing will be pushed.
|
||||
#
|
||||
# This hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- Name of the remote to which the push is being done
|
||||
# $2 -- URL to which the push is being done
|
||||
#
|
||||
# If pushing without using a named remote those arguments will be equal.
|
||||
#
|
||||
# Information about the commits which are being pushed is supplied as lines to
|
||||
# the standard input in the form:
|
||||
#
|
||||
# <local ref> <local oid> <remote ref> <remote oid>
|
||||
#
|
||||
# This sample shows how to prevent push of commits where the log message starts
|
||||
# with "WIP" (work in progress).
|
||||
|
||||
remote="$1"
|
||||
url="$2"
|
||||
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
|
||||
while read local_ref local_oid remote_ref remote_oid
|
||||
do
|
||||
if test "$local_oid" = "$zero"
|
||||
then
|
||||
# Handle delete
|
||||
:
|
||||
else
|
||||
if test "$remote_oid" = "$zero"
|
||||
then
|
||||
# New branch, examine all commits
|
||||
range="$local_oid"
|
||||
else
|
||||
# Update to existing branch, examine new commits
|
||||
range="$remote_oid..$local_oid"
|
||||
fi
|
||||
|
||||
# Check for WIP commit
|
||||
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
|
||||
if test -n "$commit"
|
||||
then
|
||||
echo >&2 "Found WIP commit in $local_ref, not pushing"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
@@ -1,169 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Copyright (c) 2006, 2008 Junio C Hamano
|
||||
#
|
||||
# The "pre-rebase" hook is run just before "git rebase" starts doing
|
||||
# its job, and can prevent the command from running by exiting with
|
||||
# non-zero status.
|
||||
#
|
||||
# The hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- the upstream the series was forked from.
|
||||
# $2 -- the branch being rebased (or empty when rebasing the current branch).
|
||||
#
|
||||
# This sample shows how to prevent topic branches that are already
|
||||
# merged to 'next' branch from getting rebased, because allowing it
|
||||
# would result in rebasing already published history.
|
||||
|
||||
publish=next
|
||||
basebranch="$1"
|
||||
if test "$#" = 2
|
||||
then
|
||||
topic="refs/heads/$2"
|
||||
else
|
||||
topic=`git symbolic-ref HEAD` ||
|
||||
exit 0 ;# we do not interrupt rebasing detached HEAD
|
||||
fi
|
||||
|
||||
case "$topic" in
|
||||
refs/heads/??/*)
|
||||
;;
|
||||
*)
|
||||
exit 0 ;# we do not interrupt others.
|
||||
;;
|
||||
esac
|
||||
|
||||
# Now we are dealing with a topic branch being rebased
|
||||
# on top of master. Is it OK to rebase it?
|
||||
|
||||
# Does the topic really exist?
|
||||
git show-ref -q "$topic" || {
|
||||
echo >&2 "No such branch $topic"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Is topic fully merged to master?
|
||||
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
|
||||
if test -z "$not_in_master"
|
||||
then
|
||||
echo >&2 "$topic is fully merged to master; better remove it."
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
fi
|
||||
|
||||
# Is topic ever merged to next? If so you should not be rebasing it.
|
||||
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
|
||||
only_next_2=`git rev-list ^master ${publish} | sort`
|
||||
if test "$only_next_1" = "$only_next_2"
|
||||
then
|
||||
not_in_topic=`git rev-list "^$topic" master`
|
||||
if test -z "$not_in_topic"
|
||||
then
|
||||
echo >&2 "$topic is already up to date with master"
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
|
||||
/usr/bin/perl -e '
|
||||
my $topic = $ARGV[0];
|
||||
my $msg = "* $topic has commits already merged to public branch:\n";
|
||||
my (%not_in_next) = map {
|
||||
/^([0-9a-f]+) /;
|
||||
($1 => 1);
|
||||
} split(/\n/, $ARGV[1]);
|
||||
for my $elem (map {
|
||||
/^([0-9a-f]+) (.*)$/;
|
||||
[$1 => $2];
|
||||
} split(/\n/, $ARGV[2])) {
|
||||
if (!exists $not_in_next{$elem->[0]}) {
|
||||
if ($msg) {
|
||||
print STDERR $msg;
|
||||
undef $msg;
|
||||
}
|
||||
print STDERR " $elem->[1]\n";
|
||||
}
|
||||
}
|
||||
' "$topic" "$not_in_next" "$not_in_master"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
<<\DOC_END
|
||||
|
||||
This sample hook safeguards topic branches that have been
|
||||
published from being rewound.
|
||||
|
||||
The workflow assumed here is:
|
||||
|
||||
* Once a topic branch forks from "master", "master" is never
|
||||
merged into it again (either directly or indirectly).
|
||||
|
||||
* Once a topic branch is fully cooked and merged into "master",
|
||||
it is deleted. If you need to build on top of it to correct
|
||||
earlier mistakes, a new topic branch is created by forking at
|
||||
the tip of the "master". This is not strictly necessary, but
|
||||
it makes it easier to keep your history simple.
|
||||
|
||||
* Whenever you need to test or publish your changes to topic
|
||||
branches, merge them into "next" branch.
|
||||
|
||||
The script, being an example, hardcodes the publish branch name
|
||||
to be "next", but it is trivial to make it configurable via
|
||||
$GIT_DIR/config mechanism.
|
||||
|
||||
With this workflow, you would want to know:
|
||||
|
||||
(1) ... if a topic branch has ever been merged to "next". Young
|
||||
topic branches can have stupid mistakes you would rather
|
||||
clean up before publishing, and things that have not been
|
||||
merged into other branches can be easily rebased without
|
||||
affecting other people. But once it is published, you would
|
||||
not want to rewind it.
|
||||
|
||||
(2) ... if a topic branch has been fully merged to "master".
|
||||
Then you can delete it. More importantly, you should not
|
||||
build on top of it -- other people may already want to
|
||||
change things related to the topic as patches against your
|
||||
"master", so if you need further changes, it is better to
|
||||
fork the topic (perhaps with the same name) afresh from the
|
||||
tip of "master".
|
||||
|
||||
Let's look at this example:
|
||||
|
||||
o---o---o---o---o---o---o---o---o---o "next"
|
||||
/ / / /
|
||||
/ a---a---b A / /
|
||||
/ / / /
|
||||
/ / c---c---c---c B /
|
||||
/ / / \ /
|
||||
/ / / b---b C \ /
|
||||
/ / / / \ /
|
||||
---o---o---o---o---o---o---o---o---o---o---o "master"
|
||||
|
||||
|
||||
A, B and C are topic branches.
|
||||
|
||||
* A has one fix since it was merged up to "next".
|
||||
|
||||
* B has finished. It has been fully merged up to "master" and "next",
|
||||
and is ready to be deleted.
|
||||
|
||||
* C has not merged to "next" at all.
|
||||
|
||||
We would want to allow C to be rebased, refuse A, and encourage
|
||||
B to be deleted.
|
||||
|
||||
To compute (1):
|
||||
|
||||
git rev-list ^master ^topic next
|
||||
git rev-list ^master next
|
||||
|
||||
if these match, topic has not merged in next at all.
|
||||
|
||||
To compute (2):
|
||||
|
||||
git rev-list master..topic
|
||||
|
||||
if this is empty, it is fully merged to "master".
|
||||
|
||||
DOC_END
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to make use of push options.
|
||||
# The example simply echoes all push options that start with 'echoback='
|
||||
# and rejects all pushes when the "reject" push option is used.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-receive".
|
||||
|
||||
if test -n "$GIT_PUSH_OPTION_COUNT"
|
||||
then
|
||||
i=0
|
||||
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
|
||||
do
|
||||
eval "value=\$GIT_PUSH_OPTION_$i"
|
||||
case "$value" in
|
||||
echoback=*)
|
||||
echo "echo from the pre-receive-hook: ${value#*=}" >&2
|
||||
;;
|
||||
reject)
|
||||
exit 1
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
fi
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare the commit log message.
|
||||
# Called by "git commit" with the name of the file that has the
|
||||
# commit message, followed by the description of the commit
|
||||
# message's source. The hook's purpose is to edit the commit
|
||||
# message file. If the hook fails with a non-zero status,
|
||||
# the commit is aborted.
|
||||
#
|
||||
# To enable this hook, rename this file to "prepare-commit-msg".
|
||||
|
||||
# This hook includes three examples. The first one removes the
|
||||
# "# Please enter the commit message..." help message.
|
||||
#
|
||||
# The second includes the output of "git diff --name-status -r"
|
||||
# into the message, just before the "git status" output. It is
|
||||
# commented because it doesn't cope with --amend or with squashed
|
||||
# commits.
|
||||
#
|
||||
# The third example adds a Signed-off-by line to the message, that can
|
||||
# still be edited. This is rarely a good idea.
|
||||
|
||||
COMMIT_MSG_FILE=$1
|
||||
COMMIT_SOURCE=$2
|
||||
SHA1=$3
|
||||
|
||||
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
|
||||
|
||||
# case "$COMMIT_SOURCE,$SHA1" in
|
||||
# ,|template,)
|
||||
# /usr/bin/perl -i.bak -pe '
|
||||
# print "\n" . `git diff --cached --name-status -r`
|
||||
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
|
||||
# *) ;;
|
||||
# esac
|
||||
|
||||
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
|
||||
# if test -z "$COMMIT_SOURCE"
|
||||
# then
|
||||
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
|
||||
# fi
|
||||
@@ -1,78 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# An example hook script to update a checked-out tree on a git push.
|
||||
#
|
||||
# This hook is invoked by git-receive-pack(1) when it reacts to git
|
||||
# push and updates reference(s) in its repository, and when the push
|
||||
# tries to update the branch that is currently checked out and the
|
||||
# receive.denyCurrentBranch configuration variable is set to
|
||||
# updateInstead.
|
||||
#
|
||||
# By default, such a push is refused if the working tree and the index
|
||||
# of the remote repository has any difference from the currently
|
||||
# checked out commit; when both the working tree and the index match
|
||||
# the current commit, they are updated to match the newly pushed tip
|
||||
# of the branch. This hook is to be used to override the default
|
||||
# behaviour; however the code below reimplements the default behaviour
|
||||
# as a starting point for convenient modification.
|
||||
#
|
||||
# The hook receives the commit with which the tip of the current
|
||||
# branch is going to be updated:
|
||||
commit=$1
|
||||
|
||||
# It can exit with a non-zero status to refuse the push (when it does
|
||||
# so, it must not modify the index or the working tree).
|
||||
die () {
|
||||
echo >&2 "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Or it can make any necessary changes to the working tree and to the
|
||||
# index to bring them to the desired state when the tip of the current
|
||||
# branch is updated to the new commit, and exit with a zero status.
|
||||
#
|
||||
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
|
||||
# in order to emulate git fetch that is run in the reverse direction
|
||||
# with git push, as the two-tree form of git read-tree -u -m is
|
||||
# essentially the same as git switch or git checkout that switches
|
||||
# branches while keeping the local changes in the working tree that do
|
||||
# not interfere with the difference between the branches.
|
||||
|
||||
# The below is a more-or-less exact translation to shell of the C code
|
||||
# for the default behaviour for git's push-to-checkout hook defined in
|
||||
# the push_to_deploy() function in builtin/receive-pack.c.
|
||||
#
|
||||
# Note that the hook will be executed from the repository directory,
|
||||
# not from the working tree, so if you want to perform operations on
|
||||
# the working tree, you will have to adapt your code accordingly, e.g.
|
||||
# by adding "cd .." or using relative paths.
|
||||
|
||||
if ! git update-index -q --ignore-submodules --refresh
|
||||
then
|
||||
die "Up-to-date check failed"
|
||||
fi
|
||||
|
||||
if ! git diff-files --quiet --ignore-submodules --
|
||||
then
|
||||
die "Working directory has unstaged changes"
|
||||
fi
|
||||
|
||||
# This is a rough translation of:
|
||||
#
|
||||
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
|
||||
if git cat-file -e HEAD 2>/dev/null
|
||||
then
|
||||
head=HEAD
|
||||
else
|
||||
head=$(git hash-object -t tree --stdin </dev/null)
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet --cached --ignore-submodules $head --
|
||||
then
|
||||
die "Working directory has staged changes"
|
||||
fi
|
||||
|
||||
if ! git read-tree -u -m "$commit"
|
||||
then
|
||||
die "Could not update working tree to new HEAD"
|
||||
fi
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# An example hook script to validate a patch (and/or patch series) before
|
||||
# sending it via email.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an appropriate
|
||||
# message if it wants to prevent the email(s) from being sent.
|
||||
#
|
||||
# To enable this hook, rename this file to "sendemail-validate".
|
||||
#
|
||||
# By default, it will only check that the patch(es) can be applied on top of
|
||||
# the default upstream branch without conflicts in a secondary worktree. After
|
||||
# validation (successful or not) of the last patch of a series, the worktree
|
||||
# will be deleted.
|
||||
#
|
||||
# The following config variables can be set to change the default remote and
|
||||
# remote ref that are used to apply the patches against:
|
||||
#
|
||||
# sendemail.validateRemote (default: origin)
|
||||
# sendemail.validateRemoteRef (default: HEAD)
|
||||
#
|
||||
# Replace the TODO placeholders with appropriate checks according to your
|
||||
# needs.
|
||||
|
||||
validate_cover_letter () {
|
||||
file="$1"
|
||||
# TODO: Replace with appropriate checks (e.g. spell checking).
|
||||
true
|
||||
}
|
||||
|
||||
validate_patch () {
|
||||
file="$1"
|
||||
# Ensure that the patch applies without conflicts.
|
||||
git am -3 "$file" || return
|
||||
# TODO: Replace with appropriate checks for this patch
|
||||
# (e.g. checkpatch.pl).
|
||||
true
|
||||
}
|
||||
|
||||
validate_series () {
|
||||
# TODO: Replace with appropriate checks for the whole series
|
||||
# (e.g. quick build, coding style checks, etc.).
|
||||
true
|
||||
}
|
||||
|
||||
# main -------------------------------------------------------------------------
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
|
||||
then
|
||||
remote=$(git config --default origin --get sendemail.validateRemote) &&
|
||||
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
|
||||
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
|
||||
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
|
||||
git config --replace-all sendemail.validateWorktree "$worktree"
|
||||
else
|
||||
worktree=$(git config --get sendemail.validateWorktree)
|
||||
fi || {
|
||||
echo "sendemail-validate: error: failed to prepare worktree" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
unset GIT_DIR GIT_WORK_TREE
|
||||
cd "$worktree" &&
|
||||
|
||||
if grep -q "^diff --git " "$1"
|
||||
then
|
||||
validate_patch "$1"
|
||||
else
|
||||
validate_cover_letter "$1"
|
||||
fi &&
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
|
||||
then
|
||||
git config --unset-all sendemail.validateWorktree &&
|
||||
trap 'git worktree remove -ff "$worktree"' EXIT &&
|
||||
validate_series
|
||||
fi
|
||||
@@ -1,128 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to block unannotated tags from entering.
|
||||
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
|
||||
#
|
||||
# To enable this hook, rename this file to "update".
|
||||
#
|
||||
# Config
|
||||
# ------
|
||||
# hooks.allowunannotated
|
||||
# This boolean sets whether unannotated tags will be allowed into the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowdeletetag
|
||||
# This boolean sets whether deleting tags will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowmodifytag
|
||||
# This boolean sets whether a tag may be modified after creation. By default
|
||||
# it won't be.
|
||||
# hooks.allowdeletebranch
|
||||
# This boolean sets whether deleting branches will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.denycreatebranch
|
||||
# This boolean sets whether remotely creating branches will be denied
|
||||
# in the repository. By default this is allowed.
|
||||
#
|
||||
|
||||
# --- Command line
|
||||
refname="$1"
|
||||
oldrev="$2"
|
||||
newrev="$3"
|
||||
|
||||
# --- Safety check
|
||||
if [ -z "$GIT_DIR" ]; then
|
||||
echo "Don't run this script from the command line." >&2
|
||||
echo " (if you want, you could supply GIT_DIR then run" >&2
|
||||
echo " $0 <ref> <oldrev> <newrev>)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
|
||||
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Config
|
||||
allowunannotated=$(git config --type=bool hooks.allowunannotated)
|
||||
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
|
||||
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
|
||||
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
|
||||
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
|
||||
|
||||
# check for no description
|
||||
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
|
||||
case "$projectdesc" in
|
||||
"Unnamed repository"* | "")
|
||||
echo "*** Project description file hasn't been set" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Check types
|
||||
# if $newrev is 0000...0000, it's a commit to delete a ref.
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
if [ "$newrev" = "$zero" ]; then
|
||||
newrev_type=delete
|
||||
else
|
||||
newrev_type=$(git cat-file -t $newrev)
|
||||
fi
|
||||
|
||||
case "$refname","$newrev_type" in
|
||||
refs/tags/*,commit)
|
||||
# un-annotated tag
|
||||
short_refname=${refname##refs/tags/}
|
||||
if [ "$allowunannotated" != "true" ]; then
|
||||
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
|
||||
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,delete)
|
||||
# delete tag
|
||||
if [ "$allowdeletetag" != "true" ]; then
|
||||
echo "*** Deleting a tag is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,tag)
|
||||
# annotated tag
|
||||
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
|
||||
then
|
||||
echo "*** Tag '$refname' already exists." >&2
|
||||
echo "*** Modifying a tag is not allowed in this repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,commit)
|
||||
# branch
|
||||
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
|
||||
echo "*** Creating a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,delete)
|
||||
# delete branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/remotes/*,commit)
|
||||
# tracking branch
|
||||
;;
|
||||
refs/remotes/*,delete)
|
||||
# delete tracking branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Anything else (is there anything else?)
|
||||
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Finished
|
||||
exit 0
|
||||
@@ -1,6 +0,0 @@
|
||||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user