410 lines
12 KiB
C
410 lines
12 KiB
C
#define _GNU_SOURCE
|
|
|
|
#include "auth_envelope.h"
|
|
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include <cJSON.h>
|
|
#include <nostr_core/nip001.h>
|
|
#include <nostr_core/utils.h>
|
|
|
|
static int set_error(int *out_code,
|
|
const char **out_message,
|
|
int code,
|
|
const char *message) {
|
|
if (out_code != NULL) {
|
|
*out_code = code;
|
|
}
|
|
if (out_message != NULL) {
|
|
*out_message = message;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void auth_nonce_cache_init(auth_nonce_cache_t *cache) {
|
|
if (cache == NULL) {
|
|
return;
|
|
}
|
|
memset(cache, 0, sizeof(*cache));
|
|
}
|
|
|
|
static int auth_nonce_cache_contains(const auth_nonce_cache_t *cache, const uint8_t id[32]) {
|
|
int i;
|
|
|
|
if (cache == NULL || id == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
for (i = 0; i < cache->count; ++i) {
|
|
if (memcmp(cache->ids[i], id, 32) == 0) {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void auth_nonce_cache_insert(auth_nonce_cache_t *cache, const uint8_t id[32]) {
|
|
if (cache == NULL || id == NULL) {
|
|
return;
|
|
}
|
|
|
|
if (cache->count < AUTH_NONCE_CACHE_SIZE) {
|
|
memcpy(cache->ids[cache->count], id, 32);
|
|
cache->count++;
|
|
return;
|
|
}
|
|
|
|
memcpy(cache->ids[cache->next], id, 32);
|
|
cache->next = (cache->next + 1) % AUTH_NONCE_CACHE_SIZE;
|
|
}
|
|
|
|
static int json_item_to_compact_string(const cJSON *item, char *out, size_t out_sz) {
|
|
char *printed;
|
|
|
|
if (out == NULL || out_sz == 0 || item == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
if (cJSON_IsString(item) && item->valuestring != NULL) {
|
|
strncpy(out, item->valuestring, out_sz - 1);
|
|
out[out_sz - 1] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
printed = cJSON_PrintUnformatted((cJSON *)item);
|
|
if (printed == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
strncpy(out, printed, out_sz - 1);
|
|
out[out_sz - 1] = '\0';
|
|
free(printed);
|
|
return 0;
|
|
}
|
|
|
|
static int compute_hash_hex_for_json_item(const cJSON *item, char *out_hex, size_t out_hex_sz) {
|
|
unsigned char hash[32];
|
|
char *compact = NULL;
|
|
|
|
if (out_hex == NULL || out_hex_sz < 65) {
|
|
return -1;
|
|
}
|
|
|
|
if (item == NULL) {
|
|
compact = strdup("null");
|
|
} else {
|
|
compact = cJSON_PrintUnformatted((cJSON *)item);
|
|
}
|
|
|
|
if (compact == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
if (nostr_sha256((const unsigned char *)compact, strlen(compact), hash) != 0) {
|
|
free(compact);
|
|
return -1;
|
|
}
|
|
|
|
nostr_bytes_to_hex(hash, sizeof(hash), out_hex);
|
|
out_hex[64] = '\0';
|
|
free(compact);
|
|
return 0;
|
|
}
|
|
|
|
static int compute_params_hash_hex(cJSON *root, char *out_hex, size_t out_hex_sz) {
|
|
cJSON *params = cJSON_GetObjectItemCaseSensitive(root, "params");
|
|
return compute_hash_hex_for_json_item(params, out_hex, out_hex_sz);
|
|
}
|
|
|
|
static cJSON *find_tag_value(cJSON *tags, const char *name) {
|
|
int i;
|
|
|
|
if (!cJSON_IsArray(tags) || name == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
for (i = 0; i < cJSON_GetArraySize(tags); ++i) {
|
|
cJSON *tag = cJSON_GetArrayItem(tags, i);
|
|
cJSON *tag_name;
|
|
cJSON *tag_value;
|
|
|
|
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
|
continue;
|
|
}
|
|
|
|
tag_name = cJSON_GetArrayItem(tag, 0);
|
|
tag_value = cJSON_GetArrayItem(tag, 1);
|
|
if (!cJSON_IsString(tag_name) || tag_name->valuestring == NULL) {
|
|
continue;
|
|
}
|
|
if (strcmp(tag_name->valuestring, name) != 0) {
|
|
continue;
|
|
}
|
|
|
|
return tag_value;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static cJSON *build_tag_pair(const char *name, const char *value) {
|
|
cJSON *pair;
|
|
|
|
if (name == NULL || value == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
pair = cJSON_CreateArray();
|
|
if (pair == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
cJSON_AddItemToArray(pair, cJSON_CreateString(name));
|
|
cJSON_AddItemToArray(pair, cJSON_CreateString(value));
|
|
return pair;
|
|
}
|
|
|
|
int auth_envelope_build_for_request(const char *request_id,
|
|
const char *method,
|
|
const cJSON *params,
|
|
const unsigned char privkey[32],
|
|
const char *label,
|
|
time_t created_at,
|
|
cJSON **out_auth) {
|
|
cJSON *tags = NULL;
|
|
cJSON *auth = NULL;
|
|
char body_hash_hex[65];
|
|
|
|
if (out_auth == NULL || request_id == NULL || method == NULL ||
|
|
privkey == NULL || request_id[0] == '\0' || method[0] == '\0') {
|
|
return -1;
|
|
}
|
|
|
|
*out_auth = NULL;
|
|
|
|
if (compute_hash_hex_for_json_item(params, body_hash_hex, sizeof(body_hash_hex)) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
tags = cJSON_CreateArray();
|
|
if (tags == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddItemToArray(tags, build_tag_pair("nsigner_rpc", request_id));
|
|
cJSON_AddItemToArray(tags, build_tag_pair("nsigner_method", method));
|
|
cJSON_AddItemToArray(tags, build_tag_pair("nsigner_body_hash", body_hash_hex));
|
|
|
|
if (created_at <= 0) {
|
|
created_at = time(NULL);
|
|
}
|
|
|
|
auth = nostr_create_and_sign_event(AUTH_EVENT_KIND,
|
|
(label != NULL) ? label : "",
|
|
tags,
|
|
privkey,
|
|
created_at);
|
|
|
|
if (auth == NULL) {
|
|
cJSON_Delete(tags);
|
|
return -1;
|
|
}
|
|
|
|
*out_auth = auth;
|
|
return 0;
|
|
}
|
|
|
|
int auth_envelope_verify_request(const char *request_json,
|
|
auth_nonce_cache_t *cache,
|
|
int skew_seconds,
|
|
char *out_pubkey_hex,
|
|
size_t out_pubkey_hex_sz,
|
|
char *out_label,
|
|
size_t out_label_sz,
|
|
int *out_error_code,
|
|
const char **out_error_message) {
|
|
cJSON *root = NULL;
|
|
cJSON *auth = NULL;
|
|
cJSON *method_item;
|
|
cJSON *id_item;
|
|
cJSON *kind_item;
|
|
cJSON *created_item;
|
|
cJSON *pubkey_item;
|
|
cJSON *content_item;
|
|
cJSON *id_hex_item;
|
|
cJSON *tags_item;
|
|
cJSON *tag_rpc;
|
|
cJSON *tag_method;
|
|
cJSON *tag_body_hash;
|
|
char request_id[128];
|
|
char body_hash_hex[65];
|
|
uint8_t nonce_bytes[32];
|
|
time_t now;
|
|
long long created;
|
|
|
|
if (out_error_code != NULL) {
|
|
*out_error_code = 0;
|
|
}
|
|
if (out_error_message != NULL) {
|
|
*out_error_message = NULL;
|
|
}
|
|
|
|
if (out_pubkey_hex == NULL || out_pubkey_hex_sz < 65 ||
|
|
out_label == NULL || out_label_sz == 0 ||
|
|
request_json == NULL || cache == NULL) {
|
|
return set_error(out_error_code, out_error_message,
|
|
AUTH_ERR_ENVELOPE_MALFORMED,
|
|
"auth_envelope_malformed");
|
|
}
|
|
|
|
out_pubkey_hex[0] = '\0';
|
|
out_label[0] = '\0';
|
|
|
|
root = cJSON_Parse(request_json);
|
|
if (root == NULL) {
|
|
return set_error(out_error_code, out_error_message,
|
|
AUTH_ERR_ENVELOPE_MALFORMED,
|
|
"auth_envelope_malformed");
|
|
}
|
|
|
|
method_item = cJSON_GetObjectItemCaseSensitive(root, "method");
|
|
id_item = cJSON_GetObjectItemCaseSensitive(root, "id");
|
|
if (!cJSON_IsString(method_item) || method_item->valuestring == NULL ||
|
|
json_item_to_compact_string(id_item, request_id, sizeof(request_id)) != 0) {
|
|
cJSON_Delete(root);
|
|
return set_error(out_error_code, out_error_message,
|
|
AUTH_ERR_ENVELOPE_MALFORMED,
|
|
"auth_envelope_malformed");
|
|
}
|
|
|
|
auth = cJSON_GetObjectItemCaseSensitive(root, "auth");
|
|
if (!cJSON_IsObject(auth)) {
|
|
cJSON_Delete(root);
|
|
return set_error(out_error_code, out_error_message,
|
|
AUTH_ERR_ENVELOPE_REQUIRED,
|
|
"auth_envelope_required");
|
|
}
|
|
|
|
if (nostr_validate_event_structure(auth) != 0) {
|
|
cJSON_Delete(root);
|
|
return set_error(out_error_code, out_error_message,
|
|
AUTH_ERR_ENVELOPE_MALFORMED,
|
|
"auth_envelope_malformed");
|
|
}
|
|
|
|
if (nostr_verify_event_signature(auth) != 0) {
|
|
cJSON_Delete(root);
|
|
return set_error(out_error_code, out_error_message,
|
|
AUTH_ERR_SIGNATURE_INVALID,
|
|
"auth_signature_invalid");
|
|
}
|
|
|
|
kind_item = cJSON_GetObjectItemCaseSensitive(auth, "kind");
|
|
if (!cJSON_IsNumber(kind_item) || kind_item->valueint != AUTH_EVENT_KIND) {
|
|
cJSON_Delete(root);
|
|
return set_error(out_error_code, out_error_message,
|
|
AUTH_ERR_KIND_INVALID,
|
|
"auth_kind_invalid");
|
|
}
|
|
|
|
tags_item = cJSON_GetObjectItemCaseSensitive(auth, "tags");
|
|
tag_rpc = find_tag_value(tags_item, "nsigner_rpc");
|
|
tag_method = find_tag_value(tags_item, "nsigner_method");
|
|
tag_body_hash = find_tag_value(tags_item, "nsigner_body_hash");
|
|
if (!cJSON_IsString(tag_rpc) || tag_rpc->valuestring == NULL ||
|
|
!cJSON_IsString(tag_method) || tag_method->valuestring == NULL ||
|
|
!cJSON_IsString(tag_body_hash) || tag_body_hash->valuestring == NULL) {
|
|
cJSON_Delete(root);
|
|
return set_error(out_error_code, out_error_message,
|
|
AUTH_ERR_ENVELOPE_MALFORMED,
|
|
"auth_envelope_malformed");
|
|
}
|
|
|
|
if (strcmp(tag_rpc->valuestring, request_id) != 0 ||
|
|
strcmp(tag_method->valuestring, method_item->valuestring) != 0) {
|
|
cJSON_Delete(root);
|
|
return set_error(out_error_code, out_error_message,
|
|
AUTH_ERR_ENVELOPE_MISMATCH,
|
|
"auth_envelope_mismatch");
|
|
}
|
|
|
|
if (compute_params_hash_hex(root, body_hash_hex, sizeof(body_hash_hex)) != 0 ||
|
|
strcasecmp(tag_body_hash->valuestring, body_hash_hex) != 0) {
|
|
cJSON_Delete(root);
|
|
return set_error(out_error_code, out_error_message,
|
|
AUTH_ERR_BODY_MISMATCH,
|
|
"auth_body_mismatch");
|
|
}
|
|
|
|
created_item = cJSON_GetObjectItemCaseSensitive(auth, "created_at");
|
|
if (!cJSON_IsNumber(created_item)) {
|
|
cJSON_Delete(root);
|
|
return set_error(out_error_code, out_error_message,
|
|
AUTH_ERR_ENVELOPE_MALFORMED,
|
|
"auth_envelope_malformed");
|
|
}
|
|
|
|
if (skew_seconds <= 0) {
|
|
skew_seconds = AUTH_DEFAULT_SKEW_SECONDS;
|
|
}
|
|
|
|
now = time(NULL);
|
|
created = (long long)created_item->valuedouble;
|
|
if (llabs((long long)now - created) > (long long)skew_seconds) {
|
|
cJSON_Delete(root);
|
|
return set_error(out_error_code, out_error_message,
|
|
AUTH_ERR_ENVELOPE_STALE,
|
|
"auth_envelope_stale");
|
|
}
|
|
|
|
id_hex_item = cJSON_GetObjectItemCaseSensitive(auth, "id");
|
|
if (!cJSON_IsString(id_hex_item) || id_hex_item->valuestring == NULL ||
|
|
strlen(id_hex_item->valuestring) != 64 ||
|
|
nostr_hex_to_bytes(id_hex_item->valuestring, nonce_bytes, sizeof(nonce_bytes)) != 0) {
|
|
cJSON_Delete(root);
|
|
return set_error(out_error_code, out_error_message,
|
|
AUTH_ERR_ENVELOPE_MALFORMED,
|
|
"auth_envelope_malformed");
|
|
}
|
|
|
|
if (auth_nonce_cache_contains(cache, nonce_bytes)) {
|
|
cJSON_Delete(root);
|
|
return set_error(out_error_code, out_error_message,
|
|
AUTH_ERR_REPLAY_DETECTED,
|
|
"auth_replay_detected");
|
|
}
|
|
auth_nonce_cache_insert(cache, nonce_bytes);
|
|
|
|
pubkey_item = cJSON_GetObjectItemCaseSensitive(auth, "pubkey");
|
|
if (!cJSON_IsString(pubkey_item) || pubkey_item->valuestring == NULL ||
|
|
strlen(pubkey_item->valuestring) != 64) {
|
|
cJSON_Delete(root);
|
|
return set_error(out_error_code, out_error_message,
|
|
AUTH_ERR_ENVELOPE_MALFORMED,
|
|
"auth_envelope_malformed");
|
|
}
|
|
|
|
strncpy(out_pubkey_hex, pubkey_item->valuestring, out_pubkey_hex_sz - 1);
|
|
out_pubkey_hex[out_pubkey_hex_sz - 1] = '\0';
|
|
|
|
content_item = cJSON_GetObjectItemCaseSensitive(auth, "content");
|
|
if (cJSON_IsString(content_item) && content_item->valuestring != NULL) {
|
|
size_t i;
|
|
for (i = 0; content_item->valuestring[i] != '\0' && i + 1 < out_label_sz; ++i) {
|
|
unsigned char ch = (unsigned char)content_item->valuestring[i];
|
|
out_label[i] = (char)((isprint(ch) && ch != '\n' && ch != '\r' && ch != '\t') ? ch : ' ');
|
|
}
|
|
out_label[i] = '\0';
|
|
} else {
|
|
out_label[0] = '\0';
|
|
}
|
|
|
|
cJSON_Delete(root);
|
|
return 0;
|
|
}
|