10#include <ascii-chat/crypto/discovery_keys.h>
11#include <ascii-chat/crypto/known_hosts.h>
13#include <ascii-chat/asciichat_errno.h>
14#include <ascii-chat/common.h>
15#include <ascii-chat/crypto/keys.h>
16#include <ascii-chat/log/logging.h>
17#include <ascii-chat/platform/filesystem.h>
18#include <ascii-chat/platform/util.h>
19#include <ascii-chat/platform/question.h>
20#include <ascii-chat/platform/system.h>
21#include <ascii-chat/util/path.h>
22#include <ascii-chat/util/url.h>
40static bool is_official_server(
const char *acds_server) {
46 char server_lower[BUFFER_SIZE_SMALL];
47 SAFE_STRNCPY(server_lower, acds_server,
sizeof(server_lower));
48 for (
char *p = server_lower; *p; p++) {
49 *p = (char)tolower((
unsigned char)*p);
52 return strcmp(server_lower,
"discovery.ascii-chat.com") == 0;
60 if (!url || !pubkey_out) {
61 return SET_ERRNO(ERROR_INVALID_PARAM,
"NULL parameter in discovery_keys_download_https");
64 log_debug(
"Downloading ACDS key from %s", url);
69 if (result != ASCIICHAT_OK) {
70 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to download and parse ACDS key from %s", url);
73 memcpy(pubkey_out, key.key, 32);
74 log_debug(
"Successfully downloaded and parsed ACDS key from %s", url);
80 return SET_ERRNO(ERROR_INVALID_PARAM,
"NULL parameter in discovery_keys_load_file");
83 log_debug(
"Loading ACDS key from file: %s",
file_path);
88 if (result != ASCIICHAT_OK) {
89 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to load ACDS key from file: %s",
file_path);
92 memcpy(pubkey_out, key.key, 32);
93 log_debug(
"Successfully loaded ACDS key from file: %s",
file_path);
102 if (!username || !pubkey_out) {
103 return SET_ERRNO(ERROR_INVALID_PARAM,
"NULL parameter in discovery_keys_fetch_github");
106 log_debug(
"Fetching ACDS key from GitHub for user: %s", username);
109 char key_spec[BUFFER_SIZE_MEDIUM];
111 safe_snprintf(key_spec,
sizeof(key_spec),
"github:%s.gpg", username);
113 safe_snprintf(key_spec,
sizeof(key_spec),
"github:%s", username);
118 if (result != ASCIICHAT_OK) {
119 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to fetch ACDS key from GitHub: %s", username);
122 memcpy(pubkey_out, key.key, 32);
127 if (!username || !pubkey_out) {
128 return SET_ERRNO(ERROR_INVALID_PARAM,
"NULL parameter in discovery_keys_fetch_gitlab");
131 log_debug(
"Fetching ACDS key from GitLab for user: %s", username);
134 char key_spec[BUFFER_SIZE_MEDIUM];
135 safe_snprintf(key_spec,
sizeof(key_spec),
"gitlab:%s.gpg", username);
139 if (result != ASCIICHAT_OK) {
140 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to fetch ACDS key from GitLab: %s", username);
143 memcpy(pubkey_out, key.key, 32);
152 if (!acds_server || !path_out || path_size == 0) {
153 return SET_ERRNO(ERROR_INVALID_PARAM,
"NULL parameter in discovery_keys_get_cache_path");
158 return SET_ERRNO(ERROR_CONFIG,
"Failed to get config directory");
162 safe_snprintf(path_out, path_size,
"%s" ACDS_KEYS_CACHE_DIR PATH_SEPARATOR_STR
"%s" PATH_SEPARATOR_STR
"key.pub",
163 config_dir, acds_server);
164 SAFE_FREE(config_dir);
170 if (!acds_server || !pubkey_out) {
171 return SET_ERRNO(ERROR_INVALID_PARAM,
"NULL parameter in discovery_keys_load_cached");
176 if (result != ASCIICHAT_OK) {
182 return SET_ERRNO(ERROR_FILE_NOT_FOUND,
"No cached key for ACDS server: %s", acds_server);
190 if (!acds_server || !pubkey) {
191 return SET_ERRNO(ERROR_INVALID_PARAM,
"NULL parameter in discovery_keys_save_cached");
196 if (result != ASCIICHAT_OK) {
205 char *last_sep = strrchr(dir_path,
'\\');
207 last_sep = strrchr(dir_path,
'/');
212 if (!platform_is_directory(dir_path)) {
213 asciichat_error_t result = platform_mkdir(dir_path, 0700);
214 if (result != ASCIICHAT_OK) {
215 return SET_ERRNO(ERROR_FILE_OPERATION,
"Failed to create ACDS key cache directory: %s", dir_path);
223 return SET_ERRNO_SYS(ERROR_FILE_OPERATION,
"Failed to create cache file: %s", cache_path);
227 char base64_key[128];
228 sodium_bin2base64(base64_key,
sizeof(base64_key), pubkey, 32, sodium_base64_VARIANT_ORIGINAL);
230 fprintf(f,
"ssh-ed25519 %s acds-cached-key\n", base64_key);
233 log_debug(
"Cached ACDS key for server: %s", acds_server);
239 return SET_ERRNO(ERROR_INVALID_PARAM,
"NULL parameter in discovery_keys_clear_cache");
244 if (result != ASCIICHAT_OK) {
249 if (remove(cache_path) != 0) {
250 return SET_ERRNO_SYS(ERROR_FILE_OPERATION,
"Failed to delete cached key: %s", cache_path);
252 log_debug(
"Cleared cached ACDS key for server: %s", acds_server);
263 const uint8_t new_pubkey[32]) {
264 if (!acds_server || !old_pubkey || !new_pubkey) {
265 return SET_ERRNO(ERROR_INVALID_PARAM,
"NULL parameter in discovery_keys_verify_change");
268 char old_fingerprint[65], new_fingerprint[65];
272 log_warn(
"ACDS server key changed for: %s", acds_server);
274 log_plain_stderr(
"\n"
275 "⚠️ WARNING: ACDS SERVER KEY HAS CHANGED\n"
276 "═══════════════════════════════════════════════════════════════\n"
279 "Old key (SHA256): %s\n"
280 "New key (SHA256): %s\n"
282 "This could indicate:\n"
283 " 1. The server operator rotated their key\n"
284 " 2. A man-in-the-middle attack is in progress\n"
286 "Verify the new key fingerprint with the server operator before accepting.\n"
287 "═══════════════════════════════════════════════════════════════\n",
288 acds_server, old_fingerprint, new_fingerprint);
293 return SET_ERRNO(ERROR_CRYPTO_VERIFICATION,
"User rejected ACDS key change for: %s", acds_server);
296 log_info(
"User accepted ACDS key change for: %s", acds_server);
305 if (!acds_server || !pubkey_out) {
306 return SET_ERRNO(ERROR_INVALID_PARAM,
"NULL parameter in discovery_keys_verify");
309 uint8_t new_pubkey[32];
310 asciichat_error_t result;
311 bool is_official = is_official_server(acds_server);
317 if (!key_spec && is_official) {
319 log_info(
"Attempting automatic HTTPS key trust for official ACDS server");
323 if (result != ASCIICHAT_OK) {
324 log_debug(
"SSH key download failed, trying GPG key");
328 if (result != ASCIICHAT_OK) {
329 return SET_ERRNO(ERROR_NETWORK,
"Failed to download key from official ACDS server");
332 memcpy(new_pubkey, key.key, 32);
334 }
else if (!key_spec) {
336 return SET_ERRNO(ERROR_INVALID_PARAM,
337 "Third-party ACDS servers require explicit --discovery-service-key configuration."
338 "Only %s has automatic trust.",
339 ACDS_OFFICIAL_SERVER);
345 if (result != ASCIICHAT_OK) {
346 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to load/download ACDS key from: %s", key_spec);
349 memcpy(new_pubkey, key.key, 32);
356 uint8_t cached_pubkey[32];
359 if (result == ASCIICHAT_OK) {
361 if (memcmp(cached_pubkey, new_pubkey, 32) != 0) {
364 if (result != ASCIICHAT_OK) {
370 if (result != ASCIICHAT_OK) {
371 log_warn(
"Failed to update cached key, continuing anyway");
374 log_debug(
"ACDS key matches cached key for: %s", acds_server);
379 log_info(
"First connection to ACDS server: %s, caching key", acds_server);
381 if (result != ASCIICHAT_OK) {
382 log_warn(
"Failed to cache key, continuing anyway");
390 memcpy(pubkey_out, new_pubkey, 32);
391 log_debug(
"ACDS key verification successful for: %s", acds_server);
asciichat_error_t discovery_keys_clear_cache(const char *acds_server)
asciichat_error_t discovery_keys_load_cached(const char *acds_server, uint8_t pubkey_out[32])
asciichat_error_t discovery_keys_verify(const char *acds_server, const char *key_spec, uint8_t pubkey_out[32])
asciichat_error_t discovery_keys_download_https(const char *url, uint8_t pubkey_out[32])
asciichat_error_t discovery_keys_fetch_gitlab(const char *username, uint8_t pubkey_out[32])
asciichat_error_t discovery_keys_verify_change(const char *acds_server, const uint8_t old_pubkey[32], const uint8_t new_pubkey[32])
asciichat_error_t discovery_keys_save_cached(const char *acds_server, const uint8_t pubkey[32])
asciichat_error_t discovery_keys_load_file(const char *file_path, uint8_t pubkey_out[32])
asciichat_error_t discovery_keys_get_cache_path(const char *acds_server, char *path_out, size_t path_size)
asciichat_error_t discovery_keys_fetch_github(const char *username, bool is_gpg, uint8_t pubkey_out[32])
asciichat_error_t parse_public_key(const char *input, public_key_t *key_out)
void compute_key_fingerprint(const uint8_t key[ED25519_PUBLIC_KEY_SIZE], char fingerprint[CRYPTO_HEX_KEY_SIZE_NULL])
char file_path[PLATFORM_MAX_PATH_LENGTH]
char * get_config_dir(void)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
#define PLATFORM_MAX_PATH_LENGTH
int platform_is_regular_file(const char *path)
bool platform_prompt_yes_no(const char *question, bool default_yes)
FILE * platform_fopen(const char *filename, const char *mode)