nostrdb

an unfairly fast embedded nostr database backed by lmdb
git clone git://jb55.com/nostrdb
Log | Files | Refs | Submodules | README | LICENSE

commit f8aad4dbf2a71d47637c92c8ac015406b9b4bb2d
parent 26f7adcb6810af89956acd55d6904a6bd82b03c1
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 24 Jun 2025 10:21:22 -0700

nostrdb: calculate id in ndb_note_verify

Rogue relays could in theory attack nostrdb by replaying ids and
signatures from other notes. This fixes this weakness by calculating the
id again in ndb_note_verify.

There is no known relays exploiting this, but lets get ahead of it
before we switch to the outbox model in damus iOS/notedeck

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Msrc/nostrdb.c | 58+++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/nostrdb.h | 4++--
Mtest.c | 2+-
3 files changed, 48 insertions(+), 16 deletions(-)

diff --git a/src/nostrdb.c b/src/nostrdb.c @@ -185,6 +185,8 @@ struct ndb_ingester { struct prot_queue *writer_inbox; void *filter_context; ndb_ingest_filter_fn filter; + + int scratch_size; }; struct ndb_filter_group { @@ -2321,17 +2323,34 @@ int ndb_end_query(struct ndb_txn *txn) return mdb_txn_commit(txn->mdb_txn) == 0; } -int ndb_note_verify(void *ctx, unsigned char pubkey[32], unsigned char id[32], - unsigned char sig[64]) +int ndb_note_verify(void *ctx, unsigned char *scratch, size_t scratch_size, + struct ndb_note *note) { + unsigned char id[32]; secp256k1_xonly_pubkey xonly_pubkey; int ok; - ok = secp256k1_xonly_pubkey_parse((secp256k1_context*)ctx, &xonly_pubkey, - pubkey) != 0; + // first, we ensure the id is valid by calculating the id independently + // from what is given to us + if (!ndb_calculate_id(note, scratch, scratch_size, id)) { + ndb_debug("ndb_note_verify: scratch buffer size too small"); + return 0; + } + + if (memcmp(id, note->id, 32)) { + ndb_debug("ndb_note_verify: note id does not match!"); + return 0; + } + + // id is ok, let's check signature + + ok = secp256k1_xonly_pubkey_parse((secp256k1_context*)ctx, + &xonly_pubkey, + ndb_note_pubkey(note)) != 0; if (!ok) return 0; - ok = secp256k1_schnorrsig_verify((secp256k1_context*)ctx, sig, id, 32, + ok = secp256k1_schnorrsig_verify((secp256k1_context*)ctx, + ndb_note_sig(note), id, 32, &xonly_pubkey) > 0; if (!ok) return 0; @@ -2754,6 +2773,7 @@ static int ndb_ingester_process_note(secp256k1_context *ctx, size_t note_size, struct ndb_writer_msg *out, struct ndb_ingester *ingester, + unsigned char *scratch, const char *relay) { enum ndb_ingest_filter_action action; @@ -2774,8 +2794,8 @@ static int ndb_ingester_process_note(secp256k1_context *ctx, } else { // verify! If it's an invalid note we don't need to // bother writing it to the database - if (!ndb_note_verify(ctx, note->pubkey, note->id, note->sig)) { - ndb_debug("signature verification failed\n"); + if (!ndb_note_verify(ctx, scratch, ingester->scratch_size, note)) { + ndb_debug("note verification failed\n"); return 0; } } @@ -2871,6 +2891,7 @@ static int ndb_ingester_process_event(secp256k1_context *ctx, struct ndb_ingester *ingester, struct ndb_ingester_event *ev, struct ndb_writer_msg *out, + unsigned char *scratch, MDB_txn *read_txn) { struct ndb_tce tce; @@ -2945,7 +2966,7 @@ static int ndb_ingester_process_event(secp256k1_context *ctx, } if (!ndb_ingester_process_note(ctx, note, note_size, - out, ingester, + out, ingester, scratch, ev->relay)) { ndb_debug("failed to process note\n"); goto cleanup; @@ -2967,7 +2988,7 @@ static int ndb_ingester_process_event(secp256k1_context *ctx, } if (!ndb_ingester_process_note(ctx, note, note_size, - out, ingester, + out, ingester, scratch, ev->relay)) { ndb_debug("failed to process note\n"); goto cleanup; @@ -5599,8 +5620,13 @@ static void *ndb_ingester_thread(void *data) struct ndb_writer_msg outs[THREAD_QUEUE_BATCH], *out; int i, to_write, popped, done, any_event; MDB_txn *read_txn = NULL; + unsigned char *scratch; int rc; + // this is used in note verification and anything else that + // needs a temporary buffer + scratch = malloc(ingester->scratch_size); + ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY); //ndb_debug("started ingester thread\n"); @@ -5638,6 +5664,7 @@ static void *ndb_ingester_thread(void *data) out = &outs[to_write]; if (ndb_ingester_process_event(ctx, ingester, &msg->event, out, + scratch, read_txn)) { to_write++; } @@ -5657,6 +5684,7 @@ static void *ndb_ingester_thread(void *data) ndb_debug("quitting ingester thread\n"); secp256k1_context_destroy(ctx); + free(scratch); return NULL; } @@ -5694,6 +5722,7 @@ static int ndb_writer_init(struct ndb_writer *writer, struct ndb_lmdb *lmdb, static int ndb_ingester_init(struct ndb_ingester *ingester, struct ndb_lmdb *lmdb, struct prot_queue *writer_inbox, + int scratch_size, const struct ndb_config *config) { int elem_size, num_elems; @@ -5703,6 +5732,7 @@ static int ndb_ingester_init(struct ndb_ingester *ingester, elem_size = sizeof(struct ndb_ingester_msg); num_elems = DEFAULT_QUEUE_SIZE; + ingester->scratch_size = scratch_size; ingester->writer_inbox = writer_inbox; ingester->lmdb = lmdb; ingester->flags = config->flags; @@ -5979,7 +6009,8 @@ int ndb_init(struct ndb **pndb, const char *filename, const struct ndb_config *c return 0; } - if (!ndb_ingester_init(&ndb->ingester, &ndb->lmdb, &ndb->writer.inbox, config)) { + if (!ndb_ingester_init(&ndb->ingester, &ndb->lmdb, &ndb->writer.inbox, + config->writer_scratch_buffer_size, config)) { fprintf(stderr, "failed to initialize %d ingester thread(s)\n", config->ingester_threads); return 0; @@ -6612,7 +6643,7 @@ int ndb_filter_json(const struct ndb_filter *filter, char *buf, int buflen) return cur.p - cur.start; } -int ndb_calculate_id(struct ndb_note *note, unsigned char *buf, int buflen) { +int ndb_calculate_id(struct ndb_note *note, unsigned char *buf, int buflen, unsigned char *id) { int len; if (!(len = ndb_event_commitment(note, buf, buflen))) @@ -6620,7 +6651,7 @@ int ndb_calculate_id(struct ndb_note *note, unsigned char *buf, int buflen) { //fprintf(stderr, "%.*s\n", len, buf); - sha256((struct sha256*)note->id, buf, len); + sha256((struct sha256*)id, buf, len); return 1; } @@ -6696,7 +6727,8 @@ int ndb_builder_finalize(struct ndb_builder *builder, struct ndb_note **note, ndb_builder_set_pubkey(builder, keypair->pubkey); - if (!ndb_calculate_id(builder->note, start, end - start)) + if (!ndb_calculate_id(builder->note, start, end - start, + builder->note->id)) return 0; if (!ndb_sign_id(keypair, (*note)->id, (*note)->sig)) diff --git a/src/nostrdb.h b/src/nostrdb.h @@ -488,11 +488,11 @@ void ndb_config_set_subscription_callback(struct ndb_config *config, ndb_sub_fn void ndb_config_set_writer_scratch_buffer_size(struct ndb_config *config, int scratch_size); // HELPERS -int ndb_calculate_id(struct ndb_note *note, unsigned char *buf, int buflen); +int ndb_calculate_id(struct ndb_note *note, unsigned char *buf, int buflen, unsigned char *id); int ndb_sign_id(struct ndb_keypair *keypair, unsigned char id[32], unsigned char sig[64]); int ndb_create_keypair(struct ndb_keypair *key); int ndb_decode_key(const char *secstr, struct ndb_keypair *keypair); -int ndb_note_verify(void *secp_ctx, unsigned char pubkey[32], unsigned char id[32], unsigned char signature[64]); +int ndb_note_verify(void *secp_ctx, unsigned char *scratch, size_t scratch_size, struct ndb_note *note); // NDB int ndb_init(struct ndb **ndb, const char *dbdir, const struct ndb_config *); diff --git a/test.c b/test.c @@ -630,7 +630,7 @@ static void test_parse_contact_list() memcpy(id, ndb_note_id(note), 32); memset(ndb_note_id(note), 0, 32); - assert(ndb_calculate_id(note, json, alloc_size)); + assert(ndb_calculate_id(note, json, alloc_size, ndb_note_id(note))); assert(!memcmp(ndb_note_id(note), id, 32)); const char* expected_content =