From 13fd364ca313f475b18e6187ce71406eddf69d42 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 19 Mar 2026 18:45:17 -0400 Subject: [PATCH] Add Cashu blinding helpers and secp256k1 point ops --- nostr_core/cashu_mint.c | 153 ++++++++++++++++++++++++++++ nostr_core/cashu_mint.h | 10 ++ nostr_core/crypto/nostr_secp256k1.c | 71 +++++++++++++ 3 files changed, 234 insertions(+) diff --git a/nostr_core/cashu_mint.c b/nostr_core/cashu_mint.c index 709f23a7..0c0aa4a6 100644 --- a/nostr_core/cashu_mint.c +++ b/nostr_core/cashu_mint.c @@ -10,6 +10,22 @@ #include #include +/* private secp256k1 wrappers from utils.c */ +typedef struct { + unsigned char data[64]; +} nostr_secp256k1_pubkey; + +int nostr_secp256k1_ec_pubkey_parse(nostr_secp256k1_pubkey* pubkey, const unsigned char* input, size_t inputlen); +int nostr_secp256k1_ec_pubkey_create(nostr_secp256k1_pubkey* pubkey, const unsigned char* privkey); +int nostr_secp256k1_ec_pubkey_serialize_compressed(unsigned char* output, const nostr_secp256k1_pubkey* pubkey); +int nostr_secp256k1_ec_seckey_verify(const unsigned char* seckey); +int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len); +int nostr_secp256k1_ec_pubkey_tweak_mul(nostr_secp256k1_pubkey* pubkey, const unsigned char* tweak32); +int nostr_secp256k1_ec_pubkey_negate(nostr_secp256k1_pubkey* pubkey); +int nostr_secp256k1_ec_pubkey_combine(nostr_secp256k1_pubkey* out, + const nostr_secp256k1_pubkey* const* ins, + size_t n); + #include typedef struct { @@ -1270,3 +1286,140 @@ void cashu_free_decoded_token(cashu_decoded_token_t* token) { free(token->proofs); memset(token, 0, sizeof(*token)); } + +static int cashu_hash_to_curve_pubkey(const char* secret, nostr_secp256k1_pubkey* out_y) { + if (!secret || !out_y) return NOSTR_ERROR_INVALID_INPUT; + + unsigned char digest[32] = {0}; + if (nostr_sha256((const unsigned char*)secret, strlen(secret), digest) != 0) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + + unsigned char candidate[33] = {0}; + for (int counter = 0; counter < 1024; counter++) { + unsigned char h[32] = {0}; + unsigned char data[36] = {0}; + memcpy(data, digest, 32); + data[32] = (unsigned char)((counter >> 24) & 0xff); + data[33] = (unsigned char)((counter >> 16) & 0xff); + data[34] = (unsigned char)((counter >> 8) & 0xff); + data[35] = (unsigned char)(counter & 0xff); + if (nostr_sha256(data, sizeof(data), h) != 0) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + + candidate[0] = 0x02; + memcpy(candidate + 1, h, 32); + if (nostr_secp256k1_ec_pubkey_parse(out_y, candidate, sizeof(candidate))) { + return NOSTR_SUCCESS; + } + + candidate[0] = 0x03; + if (nostr_secp256k1_ec_pubkey_parse(out_y, candidate, sizeof(candidate))) { + return NOSTR_SUCCESS; + } + } + + return NOSTR_ERROR_CRYPTO_FAILED; +} + +int cashu_blind_message(const char* secret, + const char* mint_pubkey_hex, + char out_B_hex[67], + unsigned char out_r[32]) { + if (!secret || !mint_pubkey_hex || !out_B_hex || !out_r) { + return NOSTR_ERROR_INVALID_INPUT; + } + + unsigned char mint_pubkey[33] = {0}; + if (strlen(mint_pubkey_hex) != 66 || nostr_hex_to_bytes(mint_pubkey_hex, mint_pubkey, 33) != 0) { + return NOSTR_ERROR_INVALID_INPUT; + } + + nostr_secp256k1_pubkey Y; + if (cashu_hash_to_curve_pubkey(secret, &Y) != NOSTR_SUCCESS) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + + unsigned char r[32] = {0}; + int tries = 0; + do { + if (!nostr_secp256k1_get_random_bytes(r, sizeof(r))) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + tries++; + if (tries > 64) return NOSTR_ERROR_CRYPTO_FAILED; + } while (!nostr_secp256k1_ec_seckey_verify(r)); + + nostr_secp256k1_pubkey rG; + if (!nostr_secp256k1_ec_pubkey_create(&rG, r)) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + + const nostr_secp256k1_pubkey* pts[2] = { &Y, &rG }; + nostr_secp256k1_pubkey B; + if (!nostr_secp256k1_ec_pubkey_combine(&B, pts, 2)) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + + unsigned char B_bytes[33] = {0}; + if (!nostr_secp256k1_ec_pubkey_serialize_compressed(B_bytes, &B)) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + + nostr_bytes_to_hex(B_bytes, 33, out_B_hex); + memcpy(out_r, r, 32); + return NOSTR_SUCCESS; +} + +int cashu_unblind_signature(const char* C_blinded_hex, + const char* mint_pubkey_hex, + const unsigned char r[32], + char out_C_hex[67]) { + if (!C_blinded_hex || !mint_pubkey_hex || !r || !out_C_hex) { + return NOSTR_ERROR_INVALID_INPUT; + } + + unsigned char Cb_bytes[33] = {0}; + unsigned char M_bytes[33] = {0}; + + if (strlen(C_blinded_hex) != 66 || nostr_hex_to_bytes(C_blinded_hex, Cb_bytes, 33) != 0) { + return NOSTR_ERROR_INVALID_INPUT; + } + if (strlen(mint_pubkey_hex) != 66 || nostr_hex_to_bytes(mint_pubkey_hex, M_bytes, 33) != 0) { + return NOSTR_ERROR_INVALID_INPUT; + } + if (!nostr_secp256k1_ec_seckey_verify(r)) { + return NOSTR_ERROR_INVALID_INPUT; + } + + nostr_secp256k1_pubkey Cb; + nostr_secp256k1_pubkey K; + if (!nostr_secp256k1_ec_pubkey_parse(&Cb, Cb_bytes, 33)) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + if (!nostr_secp256k1_ec_pubkey_parse(&K, M_bytes, 33)) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + + if (!nostr_secp256k1_ec_pubkey_tweak_mul(&K, r)) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + if (!nostr_secp256k1_ec_pubkey_negate(&K)) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + + const nostr_secp256k1_pubkey* pts[2] = { &Cb, &K }; + nostr_secp256k1_pubkey C; + if (!nostr_secp256k1_ec_pubkey_combine(&C, pts, 2)) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + + unsigned char out_bytes[33] = {0}; + if (!nostr_secp256k1_ec_pubkey_serialize_compressed(out_bytes, &C)) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + + nostr_bytes_to_hex(out_bytes, 33, out_C_hex); + return NOSTR_SUCCESS; +} diff --git a/nostr_core/cashu_mint.h b/nostr_core/cashu_mint.h index c650b432..50ab6f6e 100644 --- a/nostr_core/cashu_mint.h +++ b/nostr_core/cashu_mint.h @@ -214,6 +214,16 @@ int cashu_decode_token(const char* token_string, 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 diff --git a/nostr_core/crypto/nostr_secp256k1.c b/nostr_core/crypto/nostr_secp256k1.c index 5556b98f..7ac9a4fa 100644 --- a/nostr_core/crypto/nostr_secp256k1.c +++ b/nostr_core/crypto/nostr_secp256k1.c @@ -237,6 +237,77 @@ 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;