ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
discovery_keys.c
Go to the documentation of this file.
1
12
14#include <ascii-chat/common.h>
16#include <ascii-chat/log/log.h>
21#include <ascii-chat/util/url.h>
22
23#include <ctype.h>
24#include <errno.h>
25#include <sodium.h>
26#include <stdio.h>
27#include <stdlib.h>
28#include <string.h>
29#include <sys/stat.h>
30#include <sys/types.h>
31
32// ============================================================================
33// Helper Functions
34// ============================================================================
35
48static bool is_official_server(const char *acds_server) {
49 if (!acds_server) {
50 return false;
51 }
52
53 // Case-insensitive comparison
54 char server_lower[BUFFER_SIZE_SMALL];
55 SAFE_STRNCPY(server_lower, acds_server, sizeof(server_lower));
56 for (char *p = server_lower; *p; p++) {
57 *p = (char)tolower((unsigned char)*p);
58 }
59
60 // Check official production server
61 if (strcmp(server_lower, ACDS_OFFICIAL_SERVER) == 0) {
62 return true;
63 }
64
65 // In debug builds, also trust localhost for testing (IPv4 and IPv6)
66#ifndef NDEBUG
67 if (strcmp(server_lower, "localhost") == 0 || strcmp(server_lower, "127.0.0.1") == 0 ||
68 strcmp(server_lower, "::1") == 0) {
69 return true;
70 }
71#endif
72
73 return false;
74}
75
76// ============================================================================
77// HTTPS Download and Key Loading
78// ============================================================================
79
81 if (!url || !pubkey_out) {
82 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_download_https");
83 }
84
85 log_debug("Downloading ACDS key from %s", url);
86
87 // Use existing parse_public_key infrastructure which handles HTTPS URLs
88 public_key_t key;
89 asciichat_error_t result = parse_public_key(url, &key);
90 if (result != ASCIICHAT_OK) {
91 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to download and parse ACDS key from %s", url);
92 }
93
94 memcpy(pubkey_out, key.key, 32);
95 log_debug("Successfully downloaded and parsed ACDS key from %s", url);
96 return ASCIICHAT_OK;
97}
98
100 if (!file_path || !pubkey_out) {
101 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_load_file");
102 }
103
104 log_debug("Loading ACDS key from file: %s", file_path);
105
106 // Use existing parse_public_key infrastructure which handles file paths
107 public_key_t key;
109 if (result != ASCIICHAT_OK) {
110 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to load ACDS key from file: %s", file_path);
111 }
112
113 memcpy(pubkey_out, key.key, 32);
114 log_debug("Successfully loaded ACDS key from file: %s", file_path);
115 return ASCIICHAT_OK;
116}
117
118// ============================================================================
119// Key Caching
120// ============================================================================
121
122asciichat_error_t discovery_keys_get_cache_path(const char *acds_server, char *path_out, size_t path_size) {
123 if (!acds_server || !path_out || path_size == 0) {
124 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_get_cache_path");
125 }
126
127 char *config_dir = get_config_dir();
128 if (!config_dir) {
129 return SET_ERRNO(ERROR_CONFIG, "Failed to get config directory");
130 }
131
132 // Path: ~/.config/ascii-chat/acds_keys/<hostname>/key.pub
133 safe_snprintf(path_out, path_size, "%s" ACDS_KEYS_CACHE_DIR PATH_SEPARATOR_STR "%s" PATH_SEPARATOR_STR "key.pub",
134 config_dir, acds_server);
135 SAFE_FREE(config_dir);
136
137 return ASCIICHAT_OK;
138}
139
140asciichat_error_t discovery_keys_load_cached(const char *acds_server, uint8_t pubkey_out[32]) {
141 if (!acds_server || !pubkey_out) {
142 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_load_cached");
143 }
144
145 char cache_path[PLATFORM_MAX_PATH_LENGTH];
146 asciichat_error_t result = discovery_keys_get_cache_path(acds_server, cache_path, sizeof(cache_path));
147 if (result != ASCIICHAT_OK) {
148 return result;
149 }
150
151 // Check if cached key exists
152 if (!platform_is_regular_file(cache_path)) {
153 return SET_ERRNO(ERROR_FILE_NOT_FOUND, "No cached key for ACDS server: %s", acds_server);
154 }
155
156 // Load cached key using existing infrastructure
157 return discovery_keys_load_file(cache_path, pubkey_out);
158}
159
160asciichat_error_t discovery_keys_save_cached(const char *acds_server, const uint8_t pubkey[32]) {
161 if (!acds_server || !pubkey) {
162 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_save_cached");
163 }
164
165 char cache_path[PLATFORM_MAX_PATH_LENGTH];
166 asciichat_error_t result = discovery_keys_get_cache_path(acds_server, cache_path, sizeof(cache_path));
167 if (result != ASCIICHAT_OK) {
168 return result;
169 }
170
171 // Create cache directory if it doesn't exist
172 char dir_path[PLATFORM_MAX_PATH_LENGTH];
173 safe_snprintf(dir_path, sizeof(dir_path), "%s", cache_path);
174
175 // Find the last path separator (handle both / and \ for cross-platform compatibility)
176 char *last_sep = strrchr(dir_path, '\\');
177 if (!last_sep) {
178 last_sep = strrchr(dir_path, '/');
179 }
180
181 if (last_sep) {
182 *last_sep = '\0';
183 if (!platform_is_directory(dir_path)) {
184 asciichat_error_t result = platform_mkdir(dir_path, 0700);
185 if (result != ASCIICHAT_OK) {
186 return SET_ERRNO(ERROR_FILE_OPERATION, "Failed to create ACDS key cache directory: %s", dir_path);
187 }
188 }
189 }
190
191 // Save key in OpenSSH public key format
192 FILE *f = platform_fopen("file_stream", cache_path, "w");
193 if (!f) {
194 return SET_ERRNO_SYS(ERROR_FILE_OPERATION, "Failed to create cache file: %s", cache_path);
195 }
196
197 // Convert binary Ed25519 key to base64 for OpenSSH format
198 char base64_key[128];
199 sodium_bin2base64(base64_key, sizeof(base64_key), pubkey, 32, sodium_base64_VARIANT_ORIGINAL);
200
201 fprintf(f, "ssh-ed25519 %s acds-cached-key\n", base64_key);
202 fclose(f);
203
204 log_debug("Cached ACDS key for server: %s", acds_server);
205 return ASCIICHAT_OK;
206}
207
209 if (!acds_server) {
210 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_clear_cache");
211 }
212
213 char cache_path[PLATFORM_MAX_PATH_LENGTH];
214 asciichat_error_t result = discovery_keys_get_cache_path(acds_server, cache_path, sizeof(cache_path));
215 if (result != ASCIICHAT_OK) {
216 return result;
217 }
218
219 if (platform_is_regular_file(cache_path)) {
220 if (remove(cache_path) != 0) {
221 return SET_ERRNO_SYS(ERROR_FILE_OPERATION, "Failed to delete cached key: %s", cache_path);
222 }
223 log_debug("Cleared cached ACDS key for server: %s", acds_server);
224 }
225
226 return ASCIICHAT_OK;
227}
228
229// ============================================================================
230// User Verification for Key Changes
231// ============================================================================
232
233asciichat_error_t discovery_keys_verify_change(const char *acds_server, const uint8_t old_pubkey[32],
234 const uint8_t new_pubkey[32]) {
235 if (!acds_server || !old_pubkey || !new_pubkey) {
236 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_verify_change");
237 }
238
239 char old_fingerprint[65], new_fingerprint[65];
240 compute_key_fingerprint(old_pubkey, old_fingerprint);
241 compute_key_fingerprint(new_pubkey, new_fingerprint);
242
243 const char *warning_msg = "\n"
244 "⚠️ WARNING: ACDS SERVER KEY HAS CHANGED\n"
245 "═══════════════════════════════════════════════════════════════\n"
246 "Server: %s\n"
247 "\n"
248 "Old key (SHA256): %s\n"
249 "New key (SHA256): %s\n"
250 "\n"
251 "This could indicate:\n"
252 " 1. The server operator rotated their key\n"
253 " 2. A man-in-the-middle attack is in progress\n"
254 "\n"
255 "Verify the new key fingerprint with the server operator before accepting.\n"
256 "═══════════════════════════════════════════════════════════════\n";
257
258 // Log to both stderr (via log_warn) and file log
259 log_warn(warning_msg, acds_server, old_fingerprint, new_fingerprint);
260
261 // Ask user to confirm
262 bool accepted = platform_prompt_yes_no("Accept new ACDS server key", false);
263 if (!accepted) {
264 return SET_ERRNO(ERROR_CRYPTO_VERIFICATION, "User rejected ACDS key change for: %s", acds_server);
265 }
266
267 log_info("User accepted ACDS key change for: %s", acds_server);
268 return ASCIICHAT_OK;
269}
270
271// ============================================================================
272// Main Verification Function
273// ============================================================================
274
275asciichat_error_t discovery_keys_verify(const char *acds_server, const char *key_spec, uint8_t pubkey_out[32]) {
276 if (!acds_server || !pubkey_out) {
277 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL parameter in discovery_keys_verify");
278 }
279
280 uint8_t new_pubkey[32];
281 asciichat_error_t result;
282 bool is_official = is_official_server(acds_server);
283
284 // ===========================================================================
285 // Step 1: Obtain the public key
286 // ===========================================================================
287
288 if (!key_spec && is_official) {
289 // Automatic trust for official server: try SSH key first, then GPG
290 log_info("Attempting automatic HTTPS key trust for official ACDS server");
291
292 public_key_t key;
294 if (result != ASCIICHAT_OK) {
295 log_debug("SSH key download failed, trying GPG key");
297 }
298
299 if (result != ASCIICHAT_OK) {
300 return SET_ERRNO(ERROR_NETWORK, "Failed to download key from official ACDS server");
301 }
302
303 memcpy(new_pubkey, key.key, 32);
304
305 } else if (!key_spec) {
306 // No key_spec and not official server = error
308 "Third-party ACDS servers require explicit --discovery-service-key configuration."
309 "Only %s has automatic trust.",
311
312 } else {
313 // Use parse_public_key for all other cases (handles HTTPS, files, github:, gitlab:, etc.)
314 public_key_t key;
315 result = parse_public_key(key_spec, &key);
316 if (result != ASCIICHAT_OK) {
317 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to load/download ACDS key from: %s", key_spec);
318 }
319
320 memcpy(new_pubkey, key.key, 32);
321 }
322
323 // ===========================================================================
324 // Step 2: Check cache and handle key changes
325 // ===========================================================================
326
327 uint8_t cached_pubkey[32];
328 result = discovery_keys_load_cached(acds_server, cached_pubkey);
329
330 if (result == ASCIICHAT_OK) {
331 // Cached key exists - compare
332 if (memcmp(cached_pubkey, new_pubkey, 32) != 0) {
333 // Key changed - require user verification
334 result = discovery_keys_verify_change(acds_server, cached_pubkey, new_pubkey);
335 if (result != ASCIICHAT_OK) {
336 return result; // User rejected or error
337 }
338
339 // User accepted - update cache
340 result = discovery_keys_save_cached(acds_server, new_pubkey);
341 if (result != ASCIICHAT_OK) {
342 log_warn("Failed to update cached key, continuing anyway");
343 }
344 } else {
345 log_debug("ACDS key matches cached key for: %s", acds_server);
346 }
347
348 } else {
349 // No cached key - save it for next time
350 log_info("First connection to ACDS server: %s, caching key", acds_server);
351 result = discovery_keys_save_cached(acds_server, new_pubkey);
352 if (result != ASCIICHAT_OK) {
353 log_warn("Failed to cache key, continuing anyway");
354 }
355 }
356
357 // ===========================================================================
358 // Step 3: Return verified key
359 // ===========================================================================
360
361 memcpy(pubkey_out, new_pubkey, 32);
362 log_debug("ACDS key verification successful for: %s", acds_server);
363 return ASCIICHAT_OK;
364}
⚠️‼️ Comprehensive thread-local error context system for ascii-chat
⚙️ Common definitions, error codes, macros, and types shared throughout the application
asciichat_error_t discovery_keys_clear_cache(const char *acds_server)
Clear cached key for ACDS server.
asciichat_error_t discovery_keys_load_cached(const char *acds_server, uint8_t pubkey_out[32])
Check if cached key exists and load it.
asciichat_error_t discovery_keys_verify(const char *acds_server, const char *key_spec, uint8_t pubkey_out[32])
Verify and load ACDS server public key.
asciichat_error_t discovery_keys_download_https(const char *url, uint8_t pubkey_out[32])
Download key from HTTPS URL.
asciichat_error_t discovery_keys_verify_change(const char *acds_server, const uint8_t old_pubkey[32], const uint8_t new_pubkey[32])
Verify key change with user.
asciichat_error_t discovery_keys_save_cached(const char *acds_server, const uint8_t pubkey[32])
Save key to cache.
asciichat_error_t discovery_keys_load_file(const char *file_path, uint8_t pubkey_out[32])
Load key from local file.
asciichat_error_t discovery_keys_get_cache_path(const char *acds_server, char *path_out, size_t path_size)
Get cached key path for ACDS server.
Discovery Server Public Key Trust Management.
#define ACDS_OFFICIAL_SERVER
Official ACDS server hostname (automatic HTTPS key trust)
#define ACDS_OFFICIAL_KEY_SSH_URL
Default HTTPS URLs for official ACDS keys.
#define ACDS_KEYS_CACHE_DIR
Key cache directory (relative to config dir)
#define ACDS_OFFICIAL_KEY_GPG_URL
Cross-platform error handling utilities.
Cross-platform filesystem operations.
#define BUFFER_SIZE_SMALL
Small buffer size (256 bytes)
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
unsigned char uint8_t
Definition common.h:56
#define SET_ERRNO_SYS(code, context_msg,...)
Set error code with custom message and system error context, returning the error code.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_CRYPTO_KEY
Definition error_codes.h:97
@ ERROR_FILE_NOT_FOUND
@ ERROR_NETWORK
Definition error_codes.h:77
@ ERROR_CRYPTO_VERIFICATION
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_CONFIG
Definition error_codes.h:57
@ ERROR_FILE_OPERATION
@ ERROR_INVALID_PARAM
uint8_t key[32]
Definition key_types.h:71
asciichat_error_t parse_public_key(const char *input, public_key_t *key_out)
Parse SSH/GPG public key from any format (returns first key only)
Definition keys.c:28
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
asciichat_error_t platform_mkdir(const char *path, int mode)
Create a directory.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
bool platform_prompt_yes_no(const char *question, bool default_yes)
Prompt the user for a yes/no answer.
Definition util.c:83
#define PATH_SEPARATOR_STR
Definition filesystem.h:113
int platform_is_directory(const char *path)
Check if a path is a directory.
int platform_is_regular_file(const char *path)
Check if a path is a regular file.
Definition util.c:124
FILE * platform_fopen(const char *name, const char *filename, const char *mode)
Safe file open stream (fopen replacement)
char * get_config_dir(void)
Get configuration directory path with XDG_CONFIG_HOME support.
Definition path.c:527
void compute_key_fingerprint(const uint8_t key[ED25519_PUBLIC_KEY_SIZE], char fingerprint[CRYPTO_HEX_KEY_SIZE_NULL])
Known hosts management for MITM attack prevention.
📝 Logging API with multiple log levels and terminal output control
char file_path[PLATFORM_MAX_PATH_LENGTH]
Definition mmap.c:39
📂 Path Manipulation Utilities
Cross-platform interactive prompting utilities.
Public key structure.
Definition key_types.h:69
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:69
Cross-platform system functions interface for ascii-chat.
🌐 Production-Grade URL Validation and Parsing