ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
discovery_keys.c
Go to the documentation of this file.
1
10#include <ascii-chat/crypto/discovery_keys.h>
11#include <ascii-chat/crypto/known_hosts.h>
12
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>
23
24#include <ctype.h>
25#include <errno.h>
26#include <sodium.h>
27#include <stdio.h>
28#include <stdlib.h>
29#include <string.h>
30#include <sys/stat.h>
31#include <sys/types.h>
32
33// ============================================================================
34// Helper Functions
35// ============================================================================
36
40static bool is_official_server(const char *acds_server) {
41 if (!acds_server) {
42 return false;
43 }
44
45 // Case-insensitive comparison
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);
50 }
51
52 return strcmp(server_lower, "discovery.ascii-chat.com") == 0;
53}
54
55// ============================================================================
56// HTTPS Download and Key Loading
57// ============================================================================
58
59asciichat_error_t discovery_keys_download_https(const char *url, uint8_t pubkey_out[32]) {
60 if (!url || !pubkey_out) {
61 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_download_https");
62 }
63
64 log_debug("Downloading ACDS key from %s", url);
65
66 // Use existing parse_public_key infrastructure which handles HTTPS URLs
67 public_key_t key;
68 asciichat_error_t result = parse_public_key(url, &key);
69 if (result != ASCIICHAT_OK) {
70 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to download and parse ACDS key from %s", url);
71 }
72
73 memcpy(pubkey_out, key.key, 32);
74 log_debug("Successfully downloaded and parsed ACDS key from %s", url);
75 return ASCIICHAT_OK;
76}
77
78asciichat_error_t discovery_keys_load_file(const char *file_path, uint8_t pubkey_out[32]) {
79 if (!file_path || !pubkey_out) {
80 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_load_file");
81 }
82
83 log_debug("Loading ACDS key from file: %s", file_path);
84
85 // Use existing parse_public_key infrastructure which handles file paths
86 public_key_t key;
87 asciichat_error_t result = parse_public_key(file_path, &key);
88 if (result != ASCIICHAT_OK) {
89 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to load ACDS key from file: %s", file_path);
90 }
91
92 memcpy(pubkey_out, key.key, 32);
93 log_debug("Successfully loaded ACDS key from file: %s", file_path);
94 return ASCIICHAT_OK;
95}
96
97// ============================================================================
98// GitHub/GitLab Fetching
99// ============================================================================
100
101asciichat_error_t discovery_keys_fetch_github(const char *username, bool is_gpg, uint8_t pubkey_out[32]) {
102 if (!username || !pubkey_out) {
103 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_fetch_github");
104 }
105
106 log_debug("Fetching ACDS key from GitHub for user: %s", username);
107
108 // Use existing parse_public_key infrastructure
109 char key_spec[BUFFER_SIZE_MEDIUM];
110 if (is_gpg) {
111 safe_snprintf(key_spec, sizeof(key_spec), "github:%s.gpg", username);
112 } else {
113 safe_snprintf(key_spec, sizeof(key_spec), "github:%s", username);
114 }
115
116 public_key_t key;
117 asciichat_error_t result = parse_public_key(key_spec, &key);
118 if (result != ASCIICHAT_OK) {
119 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to fetch ACDS key from GitHub: %s", username);
120 }
121
122 memcpy(pubkey_out, key.key, 32);
123 return ASCIICHAT_OK;
124}
125
126asciichat_error_t discovery_keys_fetch_gitlab(const char *username, uint8_t pubkey_out[32]) {
127 if (!username || !pubkey_out) {
128 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_fetch_gitlab");
129 }
130
131 log_debug("Fetching ACDS key from GitLab for user: %s", username);
132
133 // Use existing parse_public_key infrastructure
134 char key_spec[BUFFER_SIZE_MEDIUM];
135 safe_snprintf(key_spec, sizeof(key_spec), "gitlab:%s.gpg", username);
136
137 public_key_t key;
138 asciichat_error_t result = parse_public_key(key_spec, &key);
139 if (result != ASCIICHAT_OK) {
140 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to fetch ACDS key from GitLab: %s", username);
141 }
142
143 memcpy(pubkey_out, key.key, 32);
144 return ASCIICHAT_OK;
145}
146
147// ============================================================================
148// Key Caching
149// ============================================================================
150
151asciichat_error_t discovery_keys_get_cache_path(const char *acds_server, char *path_out, size_t path_size) {
152 if (!acds_server || !path_out || path_size == 0) {
153 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_get_cache_path");
154 }
155
156 char *config_dir = get_config_dir();
157 if (!config_dir) {
158 return SET_ERRNO(ERROR_CONFIG, "Failed to get config directory");
159 }
160
161 // Path: ~/.config/ascii-chat/acds_keys/<hostname>/key.pub
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);
165
166 return ASCIICHAT_OK;
167}
168
169asciichat_error_t discovery_keys_load_cached(const char *acds_server, uint8_t pubkey_out[32]) {
170 if (!acds_server || !pubkey_out) {
171 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_load_cached");
172 }
173
174 char cache_path[PLATFORM_MAX_PATH_LENGTH];
175 asciichat_error_t result = discovery_keys_get_cache_path(acds_server, cache_path, sizeof(cache_path));
176 if (result != ASCIICHAT_OK) {
177 return result;
178 }
179
180 // Check if cached key exists
181 if (!platform_is_regular_file(cache_path)) {
182 return SET_ERRNO(ERROR_FILE_NOT_FOUND, "No cached key for ACDS server: %s", acds_server);
183 }
184
185 // Load cached key using existing infrastructure
186 return discovery_keys_load_file(cache_path, pubkey_out);
187}
188
189asciichat_error_t discovery_keys_save_cached(const char *acds_server, const uint8_t pubkey[32]) {
190 if (!acds_server || !pubkey) {
191 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_save_cached");
192 }
193
194 char cache_path[PLATFORM_MAX_PATH_LENGTH];
195 asciichat_error_t result = discovery_keys_get_cache_path(acds_server, cache_path, sizeof(cache_path));
196 if (result != ASCIICHAT_OK) {
197 return result;
198 }
199
200 // Create cache directory if it doesn't exist
201 char dir_path[PLATFORM_MAX_PATH_LENGTH];
202 safe_snprintf(dir_path, sizeof(dir_path), "%s", cache_path);
203
204 // Find the last path separator (handle both / and \ for cross-platform compatibility)
205 char *last_sep = strrchr(dir_path, '\\');
206 if (!last_sep) {
207 last_sep = strrchr(dir_path, '/');
208 }
209
210 if (last_sep) {
211 *last_sep = '\0';
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);
216 }
217 }
218 }
219
220 // Save key in OpenSSH public key format
221 FILE *f = platform_fopen(cache_path, "w");
222 if (!f) {
223 return SET_ERRNO_SYS(ERROR_FILE_OPERATION, "Failed to create cache file: %s", cache_path);
224 }
225
226 // Convert binary Ed25519 key to base64 for OpenSSH format
227 char base64_key[128];
228 sodium_bin2base64(base64_key, sizeof(base64_key), pubkey, 32, sodium_base64_VARIANT_ORIGINAL);
229
230 fprintf(f, "ssh-ed25519 %s acds-cached-key\n", base64_key);
231 fclose(f);
232
233 log_debug("Cached ACDS key for server: %s", acds_server);
234 return ASCIICHAT_OK;
235}
236
237asciichat_error_t discovery_keys_clear_cache(const char *acds_server) {
238 if (!acds_server) {
239 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_clear_cache");
240 }
241
242 char cache_path[PLATFORM_MAX_PATH_LENGTH];
243 asciichat_error_t result = discovery_keys_get_cache_path(acds_server, cache_path, sizeof(cache_path));
244 if (result != ASCIICHAT_OK) {
245 return result;
246 }
247
248 if (platform_is_regular_file(cache_path)) {
249 if (remove(cache_path) != 0) {
250 return SET_ERRNO_SYS(ERROR_FILE_OPERATION, "Failed to delete cached key: %s", cache_path);
251 }
252 log_debug("Cleared cached ACDS key for server: %s", acds_server);
253 }
254
255 return ASCIICHAT_OK;
256}
257
258// ============================================================================
259// User Verification for Key Changes
260// ============================================================================
261
262asciichat_error_t discovery_keys_verify_change(const char *acds_server, const uint8_t old_pubkey[32],
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");
266 }
267
268 char old_fingerprint[65], new_fingerprint[65];
269 compute_key_fingerprint(old_pubkey, old_fingerprint);
270 compute_key_fingerprint(new_pubkey, new_fingerprint);
271
272 log_warn("ACDS server key changed for: %s", acds_server);
273
274 log_plain_stderr("\n"
275 "⚠️ WARNING: ACDS SERVER KEY HAS CHANGED\n"
276 "═══════════════════════════════════════════════════════════════\n"
277 "Server: %s\n"
278 "\n"
279 "Old key (SHA256): %s\n"
280 "New key (SHA256): %s\n"
281 "\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"
285 "\n"
286 "Verify the new key fingerprint with the server operator before accepting.\n"
287 "═══════════════════════════════════════════════════════════════\n",
288 acds_server, old_fingerprint, new_fingerprint);
289
290 // Ask user to confirm
291 bool accepted = platform_prompt_yes_no("Accept new ACDS server key", false);
292 if (!accepted) {
293 return SET_ERRNO(ERROR_CRYPTO_VERIFICATION, "User rejected ACDS key change for: %s", acds_server);
294 }
295
296 log_info("User accepted ACDS key change for: %s", acds_server);
297 return ASCIICHAT_OK;
298}
299
300// ============================================================================
301// Main Verification Function
302// ============================================================================
303
304asciichat_error_t discovery_keys_verify(const char *acds_server, const char *key_spec, uint8_t pubkey_out[32]) {
305 if (!acds_server || !pubkey_out) {
306 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_verify");
307 }
308
309 uint8_t new_pubkey[32];
310 asciichat_error_t result;
311 bool is_official = is_official_server(acds_server);
312
313 // ===========================================================================
314 // Step 1: Obtain the public key
315 // ===========================================================================
316
317 if (!key_spec && is_official) {
318 // Automatic trust for official server: try SSH key first, then GPG
319 log_info("Attempting automatic HTTPS key trust for official ACDS server");
320
321 public_key_t key;
322 result = parse_public_key(ACDS_OFFICIAL_KEY_SSH_URL, &key);
323 if (result != ASCIICHAT_OK) {
324 log_debug("SSH key download failed, trying GPG key");
325 result = parse_public_key(ACDS_OFFICIAL_KEY_GPG_URL, &key);
326 }
327
328 if (result != ASCIICHAT_OK) {
329 return SET_ERRNO(ERROR_NETWORK, "Failed to download key from official ACDS server");
330 }
331
332 memcpy(new_pubkey, key.key, 32);
333
334 } else if (!key_spec) {
335 // No key_spec and not official server = error
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);
340
341 } else {
342 // Use parse_public_key for all other cases (handles HTTPS, files, github:, gitlab:, etc.)
343 public_key_t key;
344 result = parse_public_key(key_spec, &key);
345 if (result != ASCIICHAT_OK) {
346 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to load/download ACDS key from: %s", key_spec);
347 }
348
349 memcpy(new_pubkey, key.key, 32);
350 }
351
352 // ===========================================================================
353 // Step 2: Check cache and handle key changes
354 // ===========================================================================
355
356 uint8_t cached_pubkey[32];
357 result = discovery_keys_load_cached(acds_server, cached_pubkey);
358
359 if (result == ASCIICHAT_OK) {
360 // Cached key exists - compare
361 if (memcmp(cached_pubkey, new_pubkey, 32) != 0) {
362 // Key changed - require user verification
363 result = discovery_keys_verify_change(acds_server, cached_pubkey, new_pubkey);
364 if (result != ASCIICHAT_OK) {
365 return result; // User rejected or error
366 }
367
368 // User accepted - update cache
369 result = discovery_keys_save_cached(acds_server, new_pubkey);
370 if (result != ASCIICHAT_OK) {
371 log_warn("Failed to update cached key, continuing anyway");
372 }
373 } else {
374 log_debug("ACDS key matches cached key for: %s", acds_server);
375 }
376
377 } else {
378 // No cached key - save it for next time
379 log_info("First connection to ACDS server: %s, caching key", acds_server);
380 result = discovery_keys_save_cached(acds_server, new_pubkey);
381 if (result != ASCIICHAT_OK) {
382 log_warn("Failed to cache key, continuing anyway");
383 }
384 }
385
386 // ===========================================================================
387 // Step 3: Return verified key
388 // ===========================================================================
389
390 memcpy(pubkey_out, new_pubkey, 32);
391 log_debug("ACDS key verification successful for: %s", acds_server);
392 return ASCIICHAT_OK;
393}
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)
Definition keys.c:29
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]
Definition mmap.c:39
char * get_config_dir(void)
Definition path.c:493
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:64
int platform_is_regular_file(const char *path)
Definition util.c:122
bool platform_prompt_yes_no(const char *question, bool default_yes)
Definition util.c:81
FILE * platform_fopen(const char *filename, const char *mode)