1 /*
2 * Copyright (c) 2014 Tim Ruehsen
3 * Copyright (c) 2015-2021 Free Software Foundation, Inc.
4 *
5 * This file is part of libwget.
6 *
7 * Libwget is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Lesser General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * Libwget is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public License
18 * along with libwget. If not, see <https://www.gnu.org/licenses/>.
19 *
20 *
21 * HSTS routines
22 *
23 * Changelog
24 * 28.01.2014 Tim Ruehsen created
25 *
26 */
27
28 #include <config.h>
29
30 #include <stdio.h>
31 #include <stdlib.h>
32 #include <string.h>
33 #include <ctype.h>
34 #include <time.h>
35 #include <errno.h>
36 #include <sys/stat.h>
37 #include <sys/file.h>
38
39 #include <wget.h>
40 #include "private.h"
41
42 /**
43 * \file
44 * \brief HTTP Strict Transport Security (RFC 6797) routines
45 * \defgroup libwget-hsts HTTP Strict Transport Security (RFC 6797) routines
46 * @{
47 *
48 * This is an implementation of RFC 6797.
49 */
50
51 struct wget_hsts_db_st {
52 const char *
53 fname;
54 wget_hashmap *
55 entries;
56 wget_thread_mutex
57 mutex;
58 int64_t
59 load_time;
60 };
61
62 typedef struct {
63 const char *
64 host;
65 int64_t
66 expires; // expiry time
67 int64_t
68 created; // creation time
69 int64_t
70 maxage; // max-age in seconds
71 uint16_t
72 port;
73 bool
74 include_subdomains : 1; // whether or not subdomains are included
75 } hsts_entry;
76
77 /// Pointer to the function table
78 static const wget_hsts_db_vtable
79 *plugin_vtable;
80
wget_hsts_set_plugin(const wget_hsts_db_vtable * vtable)81 void wget_hsts_set_plugin(const wget_hsts_db_vtable *vtable)
82 {
83 plugin_vtable = vtable;
84 }
85
86 #ifdef __clang__
87 __attribute__((no_sanitize("integer")))
88 #endif
89 WGET_GCC_PURE
hash_hsts(const hsts_entry * hsts)90 static unsigned int hash_hsts(const hsts_entry *hsts)
91 {
92 unsigned int hash = hsts->port;
93 const unsigned char *p;
94
95 for (p = (unsigned char *)hsts->host; *p; p++)
96 hash = hash * 101 + *p;
97
98 return hash;
99 }
100
101 WGET_GCC_NONNULL_ALL WGET_GCC_PURE
compare_hsts(const hsts_entry * h1,const hsts_entry * h2)102 static int compare_hsts(const hsts_entry *h1, const hsts_entry *h2)
103 {
104 int n;
105
106 if ((n = strcmp(h1->host, h2->host)))
107 return n;
108
109 return h1->port < h2->port ? -1 : (h1->port > h2->port ? 1 : 0);
110 }
111
init_hsts(hsts_entry * hsts)112 static hsts_entry *init_hsts(hsts_entry *hsts)
113 {
114 if (!hsts) {
115 if (!(hsts = wget_calloc(1, sizeof(hsts_entry))))
116 return NULL;
117 } else
118 memset(hsts, 0, sizeof(*hsts));
119
120 hsts->created = time(NULL);
121
122 return hsts;
123 }
124
deinit_hsts(hsts_entry * hsts)125 static void deinit_hsts(hsts_entry *hsts)
126 {
127 if (hsts) {
128 xfree(hsts->host);
129 }
130 }
131
free_hsts(hsts_entry * hsts)132 static void free_hsts(hsts_entry *hsts)
133 {
134 if (hsts) {
135 deinit_hsts(hsts);
136 xfree(hsts);
137 }
138 }
139
new_hsts(const char * host,uint16_t port,int64_t maxage,bool include_subdomains)140 static hsts_entry *new_hsts(const char *host, uint16_t port, int64_t maxage, bool include_subdomains)
141 {
142 hsts_entry *hsts = init_hsts(NULL);
143
144 if (!hsts)
145 return NULL;
146
147 hsts->host = wget_strdup(host);
148 hsts->port = port ? port : 443;
149 hsts->include_subdomains = include_subdomains;
150
151 if (maxage <= 0 || maxage >= INT64_MAX / 2 || hsts->created < 0 || hsts->created >= INT64_MAX / 2) {
152 hsts->maxage = 0;
153 hsts->expires = 0;
154 } else {
155 hsts->maxage = maxage;
156 hsts->expires = hsts->created + maxage;
157 }
158
159 return hsts;
160 }
161
162 /**
163 * \param[in] hsts_db An HSTS database
164 * \param[in] host Hostname to search for
165 * \param[in] port Port number in the original URI/IRI.
166 * Port number 80 is treated similar to 443, as 80 is default port for HTTP.
167 * \return 1 if the host must be accessed only through TLS, 0 if there is no such condition.
168 *
169 * Searches for a given host in the database for any previously added entry.
170 *
171 * HSTS entries older than amount of time specified by `maxage` are considered `expired` and are ignored.
172 *
173 * This function is thread-safe and can be called from multiple threads concurrently.
174 * Any implementation for this function must be thread-safe as well.
175 */
wget_hsts_host_match(const wget_hsts_db * hsts_db,const char * host,uint16_t port)176 int wget_hsts_host_match(const wget_hsts_db *hsts_db, const char *host, uint16_t port)
177 {
178 if (plugin_vtable)
179 return plugin_vtable->host_match(hsts_db, host, port);
180
181 if (!hsts_db)
182 return 0;
183
184 hsts_entry hsts, *hstsp;
185 const char *p;
186 int64_t now = time(NULL);
187
188 // first look for an exact match
189 // if it's the default port, "normalize" it
190 // we assume the scheme is HTTP
191 hsts.port = (port == 80 ? 443 : port);
192 hsts.host = host;
193 if (wget_hashmap_get(hsts_db->entries, &hsts, &hstsp) && hstsp->expires >= now)
194 return 1;
195
196 // now look for a valid subdomain match
197 for (p = host; (p = strchr(p, '.')); ) {
198 hsts.host = ++p;
199 if (wget_hashmap_get(hsts_db->entries, &hsts, &hstsp)
200 && hstsp->include_subdomains && hstsp->expires >= now)
201 return 1;
202 }
203
204 return 0;
205 }
206
207 /**
208 * \param[in] hsts_db HSTS database created by wget_hsts_db_init()
209 *
210 * Frees all resources allocated for HSTS database, except for the structure itself. The `hsts_db` pointer can then
211 * be passed to wget_hsts_db_init() for reinitialization.
212 *
213 * If `hsts_db` is NULL this function does nothing.
214 *
215 * This function only works with databases created by wget_hsts_db_init().
216 */
wget_hsts_db_deinit(wget_hsts_db * hsts_db)217 void wget_hsts_db_deinit(wget_hsts_db *hsts_db)
218 {
219 if (plugin_vtable) {
220 plugin_vtable->deinit(hsts_db);
221 return;
222 }
223
224 if (hsts_db) {
225 xfree(hsts_db->fname);
226 wget_thread_mutex_lock(hsts_db->mutex);
227 wget_hashmap_free(&hsts_db->entries);
228 wget_thread_mutex_unlock(hsts_db->mutex);
229
230 wget_thread_mutex_destroy(&hsts_db->mutex);
231 }
232 }
233
234 /**
235 * \param[in] hsts_db Pointer to the HSTS database handle (will be set to NULL)
236 *
237 * Frees all resources allocated for the HSTS database.
238 *
239 * A double pointer is required because this function will set the handle (pointer) to the HPKP database to NULL
240 * to prevent potential use-after-free conditions.
241 *
242 * If `hsts_db` or pointer it points to is NULL, then the function does nothing.
243 *
244 * Newly added entries will be lost unless committed to persistent storage using wget_hsts_db_save().
245 */
wget_hsts_db_free(wget_hsts_db ** hsts_db)246 void wget_hsts_db_free(wget_hsts_db **hsts_db)
247 {
248 if (plugin_vtable) {
249 plugin_vtable->free(hsts_db);
250 return;
251 }
252
253 if (hsts_db && *hsts_db) {
254 wget_hsts_db_deinit(*hsts_db);
255 xfree(*hsts_db);
256 }
257 }
258
hsts_db_add_entry(wget_hsts_db * hsts_db,hsts_entry * hsts)259 static void hsts_db_add_entry(wget_hsts_db *hsts_db, hsts_entry *hsts)
260 {
261 if (!hsts)
262 return;
263
264 wget_thread_mutex_lock(hsts_db->mutex);
265
266 if (hsts->maxage == 0) {
267 if (wget_hashmap_remove(hsts_db->entries, hsts))
268 debug_printf("removed HSTS %s:%hu\n", hsts->host, hsts->port);
269 free_hsts(hsts);
270 hsts = NULL;
271 } else {
272 hsts_entry *old;
273
274 if (wget_hashmap_get(hsts_db->entries, hsts, &old)) {
275 if (old->created < hsts->created || old->maxage != hsts->maxage || old->include_subdomains != hsts->include_subdomains) {
276 old->created = hsts->created;
277 old->expires = hsts->expires;
278 old->maxage = hsts->maxage;
279 old->include_subdomains = hsts->include_subdomains;
280 debug_printf("update HSTS %s:%hu (maxage=%lld, includeSubDomains=%d)\n", old->host, old->port, (long long)old->maxage, old->include_subdomains);
281 }
282 free_hsts(hsts);
283 hsts = NULL;
284 } else {
285 // key and value are the same to make wget_hashmap_get() return old 'hsts'
286 // debug_printf("add HSTS %s:%hu (maxage=%lld, includeSubDomains=%d)\n", hsts->host, hsts->port, (long long)hsts->maxage, hsts->include_subdomains);
287 wget_hashmap_put(hsts_db->entries, hsts, hsts);
288 // no need to free anything here
289 }
290 }
291
292 wget_thread_mutex_unlock(hsts_db->mutex);
293 }
294
295 /**
296 * \param[in] hsts_db An HSTS database
297 * \param[in] host Hostname from where `Strict-Transport-Security` header was received
298 * \param[in] port Port number used for connecting to the host
299 * \param[in] maxage The time from now till the entry is valid, in seconds, or 0 to remove existing entry.
300 * Corresponds to the `max-age` directive in `Strict-Transport-Security` header.
301 * \param[in] include_subdomains Nonzero if `includeSubDomains` directive was present in the header, zero otherwise
302 *
303 * Add an entry to the HSTS database. An entry corresponds to the `Strict-Transport-Security` HTTP response header.
304 * Any existing entry with same `host` and `port` is replaced. If `maxage` is zero, any existing entry with
305 * matching `host` and `port` is removed.
306 *
307 * This function is thread-safe and can be called from multiple threads concurrently.
308 * Any implementation for this function must be thread-safe as well.
309 */
wget_hsts_db_add(wget_hsts_db * hsts_db,const char * host,uint16_t port,int64_t maxage,bool include_subdomains)310 void wget_hsts_db_add(wget_hsts_db *hsts_db, const char *host, uint16_t port, int64_t maxage, bool include_subdomains)
311 {
312 if (plugin_vtable) {
313 plugin_vtable->add(hsts_db, host, port, maxage, include_subdomains);
314 return;
315 }
316
317 if (hsts_db) {
318 hsts_entry *hsts = new_hsts(host, port, maxage, include_subdomains);
319
320 hsts_db_add_entry(hsts_db, hsts);
321 }
322 }
323
hsts_db_load(wget_hsts_db * hsts_db,FILE * fp)324 static int hsts_db_load(wget_hsts_db *hsts_db, FILE *fp)
325 {
326 hsts_entry hsts;
327 struct stat st;
328 char *buf = NULL, *linep, *p;
329 size_t bufsize = 0;
330 ssize_t buflen;
331 int64_t now = time(NULL);
332 int ok;
333
334 // if the database file hasn't changed since the last read
335 // there's no need to reload
336
337 if (fstat(fileno(fp), &st) == 0) {
338 if (st.st_mtime != hsts_db->load_time)
339 hsts_db->load_time = st.st_mtime;
340 else
341 return 0;
342 }
343
344 while ((buflen = wget_getline(&buf, &bufsize, fp)) >= 0) {
345 linep = buf;
346
347 while (isspace(*linep)) linep++; // ignore leading whitespace
348 if (!*linep) continue; // skip empty lines
349
350 if (*linep == '#')
351 continue; // skip comments
352
353 // strip off \r\n
354 while (buflen > 0 && (buf[buflen] == '\n' || buf[buflen] == '\r'))
355 buf[--buflen] = 0;
356
357 init_hsts(&hsts);
358 ok = 0;
359
360 // parse host
361 if (*linep) {
362 for (p = linep; *linep && !isspace(*linep); )
363 linep++;
364 hsts.host = wget_strmemdup(p, linep - p);
365 }
366
367 // parse port
368 if (*linep) {
369 for (p = ++linep; *linep && !isspace(*linep); )
370 linep++;
371 hsts.port = (uint16_t) atoi(p);
372 if (hsts.port == 0)
373 hsts.port = 443;
374 }
375
376 // parse includeSubDomains
377 if (*linep) {
378 for (p = ++linep; *linep && !isspace(*linep); )
379 linep++;
380 hsts.include_subdomains = atoi(p) ? 1 : 0;
381 }
382
383 // parse creation time
384 if (*linep) {
385 for (p = ++linep; *linep && !isspace(*linep); )
386 linep++;
387 hsts.created = atoll(p);
388 if (hsts.created < 0 || hsts.created >= INT64_MAX / 2)
389 hsts.created = 0;
390 }
391
392 // parse max age
393 if (*linep) {
394 for (p = ++linep; *linep && !isspace(*linep); )
395 linep++;
396 hsts.maxage = atoll(p);
397 if (hsts.maxage < 0 || hsts.maxage >= INT64_MAX / 2)
398 hsts.maxage = 0; // avoid integer overflow here
399 hsts.expires = hsts.maxage ? hsts.created + hsts.maxage : 0;
400 if (hsts.expires < now) {
401 // drop expired entry
402 deinit_hsts(&hsts);
403 continue;
404 }
405 ok = 1;
406 }
407
408 if (ok) {
409 hsts_db_add_entry(hsts_db, wget_memdup(&hsts, sizeof(hsts)));
410 } else {
411 deinit_hsts(&hsts);
412 error_printf(_("Failed to parse HSTS line: '%s'\n"), buf);
413 }
414 }
415
416 xfree(buf);
417
418 if (ferror(fp)) {
419 hsts_db->load_time = 0; // reload on next call to this function
420 return -1;
421 }
422
423 return 0;
424 }
425
426 /**
427 * \param[in] hsts_db An HSTS database
428 * \return 0 if the operation succeeded, -1 in case of error
429 *
430 * Performs all operations necessary to access the HSTS database entries from persistent storage
431 * using wget_hsts_host_match() for example.
432 *
433 * For database created by wget_hsts_db_init() this function will load all the entries from the file specified
434 * in `fname` parameter of wget_hsts_db_init().
435 *
436 * If `hsts_db` is NULL this function does nothing and returns 0.
437 */
wget_hsts_db_load(wget_hsts_db * hsts_db)438 int wget_hsts_db_load(wget_hsts_db *hsts_db)
439 {
440 if (plugin_vtable)
441 return plugin_vtable->load(hsts_db);
442
443 if (!hsts_db)
444 return -1;
445
446 if (!hsts_db->fname || !*hsts_db->fname)
447 return 0;
448
449 // Load the HSTS cache from a flat file
450 // Protected by flock()
451 if (wget_update_file(hsts_db->fname, (wget_update_load_fn *) hsts_db_load, NULL, hsts_db)) {
452 error_printf(_("Failed to read HSTS data\n"));
453 return -1;
454 } else {
455 debug_printf("Fetched HSTS data from '%s'\n", hsts_db->fname);
456 return 0;
457 }
458 }
459
460 WGET_GCC_NONNULL_ALL
hsts_save(FILE * fp,const hsts_entry * hsts)461 static int hsts_save(FILE *fp, const hsts_entry *hsts)
462 {
463 wget_fprintf(fp, "%s %hu %d %lld %lld\n", hsts->host, hsts->port, hsts->include_subdomains, (long long)hsts->created, (long long)hsts->maxage);
464 return 0;
465 }
466
hsts_db_save(void * hsts_db,FILE * fp)467 static int hsts_db_save(void *hsts_db, FILE *fp)
468 {
469 wget_hashmap *entries = ((wget_hsts_db *) hsts_db)->entries;
470
471 if (wget_hashmap_size(entries) > 0) {
472 fputs("#HSTS 1.0 file\n", fp);
473 fputs("#Generated by libwget " PACKAGE_VERSION ". Edit at your own risk.\n", fp);
474 fputs("# <hostname> <port> <incl. subdomains> <created> <max-age>\n", fp);
475
476 wget_hashmap_browse(entries, (wget_hashmap_browse_fn *) hsts_save, fp);
477
478 if (ferror(fp))
479 return -1;
480 }
481
482 return 0;
483 }
484
485 /**
486 * \param[in] hsts_db HSTS database
487 * \return 0 if the operation succeeded, -1 otherwise
488 *
489 * Saves all changes to the HSTS database (via wget_hsts_db_add() for example) to persistent storage.
490 *
491 * For databases created by wget_hsts_db_init(), the data is stored into file specified by `fname` parameter
492 * of wget_hsts_db_init().
493 *
494 * If `hsts_db` is NULL this function does nothing.
495 */
wget_hsts_db_save(wget_hsts_db * hsts_db)496 int wget_hsts_db_save(wget_hsts_db *hsts_db)
497 {
498 int size;
499
500 if (plugin_vtable)
501 return plugin_vtable->save(hsts_db);
502
503 if (!hsts_db)
504 return -1;
505
506 if (!hsts_db->fname || !*hsts_db->fname)
507 return -1;
508
509 // Save the HSTS cache to a flat file
510 // Protected by flock()
511 if (wget_update_file(hsts_db->fname, (wget_update_load_fn *) hsts_db_load, hsts_db_save, hsts_db)) {
512 error_printf(_("Failed to write HSTS file '%s'\n"), hsts_db->fname);
513 return -1;
514 }
515
516 if ((size = wget_hashmap_size(hsts_db->entries)))
517 debug_printf("Saved %d HSTS entr%s into '%s'\n", size, size != 1 ? "ies" : "y", hsts_db->fname);
518 else
519 debug_printf("No HSTS entries to save. Table is empty.\n");
520
521 return 0;
522 }
523
524 /**
525 * \param[in] hsts_db Previously created HSTS database on which wget_hsts_db_deinit() has been called, or NULL
526 * \param[in] fname The file where the data is stored, or NULL.
527 * \return A new wget_hsts_db
528 *
529 * Constructor for the default implementation of HSTS database.
530 *
531 * This function does no file IO, data is read only when \ref wget_hsts_db_load "wget_hsts_db_load()" is called.
532 */
wget_hsts_db_init(wget_hsts_db * hsts_db,const char * fname)533 wget_hsts_db *wget_hsts_db_init(wget_hsts_db *hsts_db, const char *fname)
534 {
535 if (plugin_vtable)
536 return plugin_vtable->init(hsts_db, fname);
537
538 if (fname) {
539 if (!(fname = wget_strdup(fname)))
540 return NULL;
541 }
542
543 wget_hashmap *entries = wget_hashmap_create(16, (wget_hashmap_hash_fn *) hash_hsts, (wget_hashmap_compare_fn *) compare_hsts);
544 if (!entries) {
545 xfree(fname);
546 return NULL;
547 }
548
549 if (!hsts_db) {
550 if (!(hsts_db = wget_calloc(1, sizeof(struct wget_hsts_db_st)))) {
551 wget_hashmap_free(&entries);
552 xfree(fname);
553 return NULL;
554 }
555 } else
556 memset(hsts_db, 0, sizeof(*hsts_db));
557
558 hsts_db->fname = fname;
559 hsts_db->entries = entries;
560 wget_hashmap_set_key_destructor(hsts_db->entries, (wget_hashmap_key_destructor *) free_hsts);
561 wget_hashmap_set_value_destructor(hsts_db->entries, (wget_hashmap_value_destructor *) free_hsts);
562 wget_thread_mutex_init(&hsts_db->mutex);
563
564 return hsts_db;
565 }
566
567 /**
568 * \param[in] hsts_db HSTS database created by wget_hsts_db_init().
569 * \param[in] fname Filename where database should be stored, or NULL
570 *
571 * Changes the file where HSTS database entries are stored.
572 *
573 * Works only for the HSTS databases created by wget_hsts_db_init().
574 * This function does no file IO, data is read or written only when wget_hsts_db_load() or wget_hsts_db_save()
575 * is called.
576 */
wget_hsts_db_set_fname(wget_hsts_db * hsts_db,const char * fname)577 void wget_hsts_db_set_fname(wget_hsts_db *hsts_db, const char *fname)
578 {
579 xfree(hsts_db->fname);
580 hsts_db->fname = wget_strdup(fname);
581 }
582
583 /**@}*/
584