Files
n_signer/src/auth_envelope.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;
}