upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/components/wisp_relay/relay_validator.c
diff options
context:
space:
mode:
Diffstat (limited to 'components/wisp_relay/relay_validator.c')
-rw-r--r--components/wisp_relay/relay_validator.c176
1 files changed, 176 insertions, 0 deletions
diff --git a/components/wisp_relay/relay_validator.c b/components/wisp_relay/relay_validator.c
new file mode 100644
index 0000000..eb40d22
--- /dev/null
+++ b/components/wisp_relay/relay_validator.c
@@ -0,0 +1,176 @@
1#include "relay_validator.h"
2#include "relay_types.h"
3#include "esp_log.h"
4#include "mbedtls/sha256.h"
5#include "secp256k1.h"
6#include "secp256k1_extrakeys.h"
7#include "secp256k1_schnorrsig.h"
8#include "cJSON.h"
9#include "freertos/FreeRTOS.h"
10#include "freertos/task.h"
11#include <stddef.h>
12#include <string.h>
13#include <stdlib.h>
14#include <stdio.h>
15
16static const char *TAG = "relay_validator";
17
18static int hex_to_bytes(const char *hex, size_t hex_len, uint8_t *out, size_t out_len)
19{
20 if (hex_len != out_len * 2) return -1;
21 for (size_t i = 0; i < out_len; i++) {
22 unsigned int byte;
23 if (sscanf(hex + i * 2, "%02x", &byte) != 1) return -1;
24 out[i] = (uint8_t)byte;
25 }
26 return 0;
27}
28
29static char *serialize_event_for_id(const char *event_json, size_t event_len)
30{
31 cJSON *obj = cJSON_ParseWithLength(event_json, event_len);
32 if (!obj) return NULL;
33
34 cJSON *serial = cJSON_CreateArray();
35 cJSON_AddItemToArray(serial, cJSON_CreateNumber(0));
36 cJSON_AddItemToArray(serial, cJSON_CreateString(
37 cJSON_GetObjectItem(obj, "pubkey")->valuestring));
38 cJSON_AddItemToArray(serial, cJSON_CreateNumber(
39 cJSON_GetObjectItem(obj, "created_at")->valuedouble));
40 cJSON_AddItemToArray(serial, cJSON_CreateNumber(
41 cJSON_GetObjectItem(obj, "kind")->valueint));
42 cJSON *tags = cJSON_GetObjectItem(obj, "tags");
43 cJSON_AddItemToArray(serial, cJSON_Duplicate(tags, 1));
44 cJSON_AddItemToArray(serial, cJSON_CreateString(
45 cJSON_GetObjectItem(obj, "content")->valuestring));
46
47 char *result = cJSON_PrintUnformatted(serial);
48 cJSON_Delete(serial);
49 cJSON_Delete(obj);
50 return result;
51}
52
53static bool verify_event_id(const char *event_json, size_t event_len,
54 const uint8_t expected_id[32])
55{
56 char *serialized = serialize_event_for_id(event_json, event_len);
57 if (!serialized) return false;
58
59 uint8_t hash[32];
60 mbedtls_sha256((const unsigned char *)serialized, strlen(serialized), hash, 0);
61 free(serialized);
62
63 return memcmp(hash, expected_id, 32) == 0;
64}
65
66static bool verify_schnorr_sig(const uint8_t pubkey[32], const uint8_t msg[32],
67 const uint8_t sig[64])
68{
69 secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY);
70 if (!ctx) return false;
71
72 secp256k1_xonly_pubkey xonly_pub;
73 if (!secp256k1_xonly_pubkey_parse(ctx, &xonly_pub, pubkey)) {
74 secp256k1_context_destroy(ctx);
75 return false;
76 }
77
78 bool valid = secp256k1_schnorrsig_verify(ctx, sig, msg, 32, &xonly_pub);
79 secp256k1_context_destroy(ctx);
80 return valid;
81}
82
83bool relay_validator_verify_event(const char *event_json, size_t event_len)
84{
85 cJSON *obj = cJSON_ParseWithLength(event_json, event_len);
86 if (!obj) {
87 ESP_LOGD(TAG, "Invalid JSON");
88 return false;
89 }
90
91 cJSON *id_item = cJSON_GetObjectItem(obj, "id");
92 cJSON *pk_item = cJSON_GetObjectItem(obj, "pubkey");
93 cJSON *sig_item = cJSON_GetObjectItem(obj, "sig");
94
95 if (!id_item || !pk_item || !sig_item) {
96 cJSON_Delete(obj);
97 ESP_LOGD(TAG, "Missing required fields");
98 return false;
99 }
100
101 const char *id_hex = id_item->valuestring;
102 const char *pk_hex = pk_item->valuestring;
103 const char *sig_hex = sig_item->valuestring;
104
105 if (strlen(id_hex) != 64 || strlen(pk_hex) != 64 || strlen(sig_hex) != 128) {
106 cJSON_Delete(obj);
107 ESP_LOGD(TAG, "Invalid field lengths");
108 return false;
109 }
110
111 uint8_t event_id[32], pubkey[32], sig[64];
112 if (hex_to_bytes(id_hex, 64, event_id, 32) != 0 ||
113 hex_to_bytes(pk_hex, 64, pubkey, 32) != 0 ||
114 hex_to_bytes(sig_hex, 128, sig, 64) != 0) {
115 cJSON_Delete(obj);
116 ESP_LOGD(TAG, "Invalid hex encoding");
117 return false;
118 }
119
120 cJSON_Delete(obj);
121
122 if (!verify_event_id(event_json, event_len, event_id)) {
123 ESP_LOGD(TAG, "Event ID mismatch");
124 return false;
125 }
126
127 if (!verify_schnorr_sig(pubkey, event_id, sig)) {
128 ESP_LOGD(TAG, "Invalid signature");
129 return false;
130 }
131
132 return true;
133}
134
135validation_result_t relay_validator_check(const uint8_t *id,
136 const uint8_t *pubkey,
137 uint64_t created_at,
138 int kind,
139 const char *content,
140 size_t content_len,
141 const char *tags_json,
142 const uint8_t *sig,
143 const validator_config_t *config)
144{
145 (void)content; (void)content_len; (void)tags_json;
146
147 if (config) {
148 if (config->max_future_sec > 0) {
149 int64_t now = (int64_t)(xTaskGetTickCount() / configTICK_RATE_HZ);
150 if ((int64_t)created_at > now + config->max_future_sec)
151 return VALIDATION_ERR_FUTURE;
152 }
153 }
154
155 if (!verify_schnorr_sig(pubkey, id, sig))
156 return VALIDATION_ERR_SIG;
157
158 return VALIDATION_OK;
159}
160
161const char *relay_validator_result_string(validation_result_t result)
162{
163 switch (result) {
164 case VALIDATION_OK: return "ok";
165 case VALIDATION_ERR_SCHEMA: return "invalid: schema";
166 case VALIDATION_ERR_ID: return "invalid: event id";
167 case VALIDATION_ERR_SIG: return "invalid: signature";
168 case VALIDATION_ERR_EXPIRED: return "invalid: expired";
169 case VALIDATION_ERR_FUTURE: return "invalid: future";
170 case VALIDATION_ERR_DUPLICATE: return "duplicate";
171 case VALIDATION_ERR_POW: return "pow: insufficient";
172 case VALIDATION_ERR_BLOCKED: return "blocked";
173 case VALIDATION_ERR_TOO_OLD: return "invalid: too old";
174 default: return "error: unknown";
175 }
176}