1 /*
2  * Copyright (c) 2015-2021 Free Software Foundation, Inc.
3  *
4  * This file is part of libwget.
5  *
6  * Libwget is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU Lesser General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Libwget is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with libwget.  If not, see <https://www.gnu.org/licenses/>.
18  *
19  *
20  * HTTP Public Key Pinning database
21  */
22 
23 #include <config.h>
24 
25 #include <wget.h>
26 #include <string.h>
27 #include <stddef.h>
28 #include <ctype.h>
29 #include <sys/stat.h>
30 #include <limits.h>
31 #include "private.h"
32 #include "hpkp.h"
33 
34 /**
35  * \ingroup libwget-hpkp
36  *
37  * HTTP Public Key Pinning (RFC 7469) database implementation
38  *
39  * @{
40  */
41 
42 struct wget_hpkp_db_st {
43 	char *
44 		fname;
45 	wget_hashmap *
46 		entries;
47 	wget_thread_mutex
48 		mutex;
49 	int64_t
50 		load_time;
51 };
52 
53 /// Pointer to the function table
54 static const wget_hpkp_db_vtable
55 	*plugin_vtable;
56 
wget_hpkp_set_plugin(const wget_hpkp_db_vtable * vtable)57 void wget_hpkp_set_plugin(const wget_hpkp_db_vtable *vtable)
58 {
59 	plugin_vtable = vtable;
60 }
61 
62 #ifdef __clang__
63 __attribute__((no_sanitize("integer")))
64 #endif
65 WGET_GCC_PURE
hash_hpkp(const wget_hpkp * hpkp)66 static unsigned int hash_hpkp(const wget_hpkp *hpkp)
67 {
68 	unsigned int hash = 0;
69 	const unsigned char *p;
70 
71 	for (p = (unsigned char *)hpkp->host; *p; p++)
72 		hash = hash * 101 + *p; // possible integer overflow, suppression above
73 
74 	return hash;
75 }
76 
77 WGET_GCC_NONNULL_ALL WGET_GCC_PURE
compare_hpkp(const wget_hpkp * h1,const wget_hpkp * h2)78 static int compare_hpkp(const wget_hpkp *h1, const wget_hpkp *h2)
79 {
80 	return strcmp(h1->host, h2->host);
81 }
82 
83 /**
84  * \param[in] hpkp_db Pointer to the pointer of an HPKP database, provided by wget_hpkp_db_init()
85  *
86  * Frees all resources allocated for the HPKP database, except for the structure.
87  *
88  * Works only for databases created by wget_hpkp_db_init().
89  * The parameter \p hpkp_db can then be passed to \ref wget_hpkp_db_init "wget_hpkp_db_init()".
90  *
91  * If \p hpkp_db is NULL then this function does nothing.
92  */
wget_hpkp_db_deinit(wget_hpkp_db * hpkp_db)93 void wget_hpkp_db_deinit(wget_hpkp_db *hpkp_db)
94 {
95 	if (plugin_vtable) {
96 		plugin_vtable->deinit(hpkp_db);
97 		return;
98 	}
99 
100 	if (hpkp_db) {
101 		xfree(hpkp_db->fname);
102 		wget_thread_mutex_lock(hpkp_db->mutex);
103 		wget_hashmap_free(&hpkp_db->entries);
104 		wget_thread_mutex_unlock(hpkp_db->mutex);
105 
106 		wget_thread_mutex_destroy(&hpkp_db->mutex);
107 	}
108 }
109 
110 /**
111  * \param[in] hpkp_db Pointer to the pointer of an HPKP database
112  *
113  * Closes and frees the HPKP database. A double pointer is required because this function will
114  * set the handle (pointer) to the HPKP database to NULL to prevent potential use-after-free conditions.
115  *
116  * Newly added entries will be lost unless committed to persistent storage using wget_hsts_db_save().
117  *
118  * If \p hpkp_db or the pointer it points to is NULL then this function does nothing.
119  */
wget_hpkp_db_free(wget_hpkp_db ** hpkp_db)120 void wget_hpkp_db_free(wget_hpkp_db **hpkp_db)
121 {
122 	if (plugin_vtable) {
123 		plugin_vtable->free(hpkp_db);
124 		return;
125 	}
126 
127 	if (hpkp_db && *hpkp_db) {
128 		wget_hpkp_db_deinit(*hpkp_db);
129 		xfree(*hpkp_db);
130 	}
131 }
132 
133 /**
134  * \param[in] hpkp_db An HPKP database
135  * \param[in] host The hostname in question.
136  * \param[in] pubkey The public key in DER format
137  * \param[in] pubkeysize Size of `pubkey`
138  * \return  1 if both host and public key was found in the database,
139  *         -2 if host was found and public key was not found,
140  *          0 if host was not found,
141  *         -1 for any other error condition.
142  *
143  * Checks the validity of the given hostname and public key combination.
144  *
145  * This function is thread-safe and can be called from multiple threads concurrently.
146  * Any implementation for this function must be thread-safe as well.
147  */
wget_hpkp_db_check_pubkey(wget_hpkp_db * hpkp_db,const char * host,const void * pubkey,size_t pubkeysize)148 int wget_hpkp_db_check_pubkey(wget_hpkp_db *hpkp_db, const char *host, const void *pubkey, size_t pubkeysize)
149 {
150 	if (plugin_vtable)
151 		return plugin_vtable->check_pubkey(hpkp_db, host, pubkey, pubkeysize);
152 
153 	wget_hpkp key;
154 	wget_hpkp *hpkp = NULL;
155 	char digest[wget_hash_get_len(WGET_DIGTYPE_SHA256)];
156 	int subdomain = 0;
157 
158 	for (const char *domain = host; *domain && !hpkp; domain = strchrnul(domain, '.')) {
159 		while (*domain == '.')
160 			domain++;
161 
162 		key.host = domain;
163 
164 		if (!wget_hashmap_get(hpkp_db->entries, &key, &hpkp))
165 			subdomain = 1;
166 	}
167 
168 	if (!hpkp)
169 		return 0; // OK, host is not in database
170 
171 	if (subdomain && !hpkp->include_subdomains)
172 		return 0; // OK, found a matching super domain which isn't responsible for <host>
173 
174 	if (wget_hash_fast(WGET_DIGTYPE_SHA256, pubkey, pubkeysize, digest))
175 		return -1;
176 
177 	wget_hpkp_pin pinkey = { .pin = digest, .pinsize = sizeof(digest), .hash_type = "sha256" };
178 
179 	if (wget_vector_find(hpkp->pins, &pinkey) != -1)
180 		return 1; // OK, pinned pubkey found
181 
182 	return -2;
183 }
184 
185 /* We 'consume' _hpkp and thus set *_hpkp to NULL, so that the calling function
186  * can't access it any more */
187 /**
188  * \param[in] hpkp_db An HPKP database
189  * \param[in] hpkp pointer to HPKP database entry (will be set to NULL)
190  *
191  * Adds an entry to given HPKP database. The entry will replace any entry with same `host` (see wget_hpkp_set_host()).
192  * If `maxage` property of `hpkp` is zero, any existing entry with same `host` property will be removed.
193  *
194  * The database takes the ownership of the HPKP entry and the calling function must not access the entry afterwards.
195  *
196  * This function is thread-safe and can be called from multiple threads concurrently.
197  * Any implementation for this function must be thread-safe as well.
198  */
wget_hpkp_db_add(wget_hpkp_db * hpkp_db,wget_hpkp ** _hpkp)199 void wget_hpkp_db_add(wget_hpkp_db *hpkp_db, wget_hpkp **_hpkp)
200 {
201 	if (plugin_vtable) {
202 		plugin_vtable->add(hpkp_db, _hpkp);
203 		*_hpkp = NULL;
204 		return;
205 	}
206 
207 	if (!_hpkp || !*_hpkp)
208 		return;
209 
210 	wget_hpkp *hpkp = *_hpkp;
211 
212 	wget_thread_mutex_lock(hpkp_db->mutex);
213 
214 	if (hpkp->maxage == 0 || wget_vector_size(hpkp->pins) == 0) {
215 		if (wget_hashmap_remove(hpkp_db->entries, hpkp))
216 			debug_printf("removed HPKP %s\n", hpkp->host);
217 		wget_hpkp_free(hpkp);
218 	} else {
219 		wget_hpkp *old;
220 
221 		if (wget_hashmap_get(hpkp_db->entries, hpkp, &old)) {
222 			old->created = hpkp->created;
223 			old->maxage = hpkp->maxage;
224 			old->expires = hpkp->expires;
225 			old->include_subdomains = hpkp->include_subdomains;
226 			wget_vector_free(&old->pins);
227 			old->pins = hpkp->pins;
228 			hpkp->pins = NULL;
229 			debug_printf("update HPKP %s (maxage=%lld, includeSubDomains=%d)\n", old->host, (long long)old->maxage, old->include_subdomains);
230 			wget_hpkp_free(hpkp);
231 		} else {
232 			// key and value are the same to make wget_hashmap_get() return old 'hpkp'
233 			/* debug_printf("add HPKP %s (maxage=%lld, includeSubDomains=%d)\n", hpkp->host, (long long)hpkp->maxage, hpkp->include_subdomains); */
234 			wget_hashmap_put(hpkp_db->entries, hpkp, hpkp);
235 			// no need to free anything here
236 		}
237 	}
238 
239 	wget_thread_mutex_unlock(hpkp_db->mutex);
240 
241 	*_hpkp = NULL;
242 }
243 
hpkp_db_load(wget_hpkp_db * hpkp_db,FILE * fp)244 static int hpkp_db_load(wget_hpkp_db *hpkp_db, FILE *fp)
245 {
246 	int64_t created, max_age;
247 	long long _created, _max_age;
248 	int include_subdomains;
249 
250 	wget_hpkp *hpkp = NULL;
251 	struct stat st;
252 	char *buf = NULL;
253 	size_t bufsize = 0;
254 	ssize_t buflen;
255 	char hash_type[32], host[256], pin_b64[256];
256 	int64_t now = time(NULL);
257 
258 	// if the database file hasn't changed since the last read
259 	// there's no need to reload
260 
261 	if (fstat(fileno(fp), &st) == 0) {
262 		if (st.st_mtime != hpkp_db->load_time)
263 			hpkp_db->load_time = st.st_mtime;
264 		else
265 			return 0;
266 	}
267 
268 	while ((buflen = wget_getline(&buf, &bufsize, fp)) >= 0) {
269 		char *linep = buf;
270 
271 		while (isspace(*linep)) linep++; // ignore leading whitespace
272 		if (!*linep) continue; // skip empty lines
273 
274 		if (*linep == '#')
275 			continue; // skip comments
276 
277 		// strip off \r\n
278 		while (buflen > 0 && (buf[buflen] == '\n' || buf[buflen] == '\r'))
279 			buf[--buflen] = 0;
280 
281 		if (*linep != '*') {
282 			wget_hpkp_db_add(hpkp_db, &hpkp);
283 
284 			if (sscanf(linep, "%255s %d %lld %lld", host, &include_subdomains, &_created, &_max_age) == 4) {
285 				created = _created;
286 				max_age = _max_age;
287 				if (created < 0 || max_age < 0 || created >= INT64_MAX / 2 || max_age >= INT64_MAX / 2) {
288 					max_age = 0; // avoid integer overflow here
289 				}
290 				int64_t expires = created + max_age;
291 				if (max_age && expires >= now) {
292 					hpkp = wget_hpkp_new();
293 					if (hpkp) {
294 						if (!(hpkp->host = wget_strdup(host)))
295 							xfree(hpkp);
296 						else {
297 							hpkp->maxage = max_age;
298 							hpkp->created = created;
299 							hpkp->expires = expires;
300 							hpkp->include_subdomains = include_subdomains != 0;
301 						}
302 					}
303 				} else
304 					debug_printf("HPKP: entry '%s' is expired\n", host);
305 			} else {
306 				error_printf(_("HPKP: could not parse host line '%s'\n"), buf);
307 			}
308 		} else if (hpkp) {
309 			if (sscanf(linep, "*%31s %255s", hash_type, pin_b64) == 2) {
310 				wget_hpkp_pin_add(hpkp, hash_type, pin_b64);
311 			} else {
312 				error_printf(_("HPKP: could not parse pin line '%s'\n"), buf);
313 			}
314 		} else {
315 			debug_printf("HPKP: skipping PIN entry: '%s'\n", buf);
316 		}
317 	}
318 
319 	wget_hpkp_db_add(hpkp_db, &hpkp);
320 
321 	xfree(buf);
322 
323 	if (ferror(fp)) {
324 		hpkp_db->load_time = 0; // reload on next call to this function
325 		return -1;
326 	}
327 
328 	return 0;
329 }
330 
331 /**
332  * \param[in] hpkp_db Handle to an HPKP database, obtained with wget_hpkp_db_init()
333  * \return 0 on success, or a negative number on error
334  *
335  * Performs all operations necessary to access the HPKP database entries from persistent storage
336  * using wget_hpkp_db_check_pubkey() for example.
337  *
338  * For databases created by wget_hpkp_db_init() data is loaded from `fname` parameter of wget_hpkp_db_init().
339  * If this function cannot correctly parse the whole file, -1 is returned.
340  *
341  * If `hpkp_db` is NULL then this function returns 0 and does nothing else.
342  */
wget_hpkp_db_load(wget_hpkp_db * hpkp_db)343 int wget_hpkp_db_load(wget_hpkp_db *hpkp_db)
344 {
345 	if (plugin_vtable)
346 		return plugin_vtable->load(hpkp_db);
347 
348 	if (!hpkp_db)
349 		return 0;
350 
351 	if (!hpkp_db->fname || !*hpkp_db->fname)
352 		return 0;
353 
354 	if (wget_update_file(hpkp_db->fname, (wget_update_load_fn *) hpkp_db_load, NULL, hpkp_db)) {
355 		error_printf(_("Failed to read HPKP data\n"));
356 		return -1;
357 	} else {
358 		debug_printf("Fetched HPKP data from '%s'\n", hpkp_db->fname);
359 		return 0;
360 	}
361 }
362 
hpkp_save_pin(FILE * fp,wget_hpkp_pin * pin)363 static int hpkp_save_pin(FILE *fp, wget_hpkp_pin *pin)
364 {
365 	wget_fprintf(fp, "*%s %s\n", pin->hash_type, pin->pin_b64);
366 
367 	if (ferror(fp))
368 		return -1;
369 
370 	return 0;
371 }
372 
373 WGET_GCC_NONNULL_ALL
hpkp_save(FILE * fp,const wget_hpkp * hpkp)374 static int hpkp_save(FILE *fp, const wget_hpkp *hpkp)
375 {
376 	if (wget_vector_size(hpkp->pins) == 0)
377 		debug_printf("HPKP: drop '%s', no PIN entries\n", hpkp->host);
378 	else if (hpkp->expires < time(NULL))
379 		debug_printf("HPKP: drop '%s', expired\n", hpkp->host);
380 	else {
381 		wget_fprintf(fp, "%s %d %lld %lld\n", hpkp->host, hpkp->include_subdomains, (long long) hpkp->created, (long long) hpkp->maxage);
382 
383 		if (ferror(fp))
384 			return -1;
385 
386 		return wget_vector_browse(hpkp->pins, (wget_vector_browse_fn *) hpkp_save_pin, fp);
387 	}
388 
389 	return 0;
390 }
391 
hpkp_db_save(wget_hpkp_db * hpkp_db,FILE * fp)392 static int hpkp_db_save(wget_hpkp_db *hpkp_db, FILE *fp)
393 {
394 	wget_hashmap *entries = hpkp_db->entries;
395 
396 	if (wget_hashmap_size(entries) > 0) {
397 		fputs("# HPKP 1.0 file\n", fp);
398 		fputs("#Generated by libwget " PACKAGE_VERSION ". Edit at your own risk.\n", fp);
399 		fputs("#<hostname> <incl. subdomains> <created> <max-age>\n\n", fp);
400 
401 		if (ferror(fp))
402 			return -1;
403 
404 		return wget_hashmap_browse(entries, (wget_hashmap_browse_fn *) hpkp_save, fp);
405 	}
406 
407 	return 0;
408 }
409 
410 /**
411  * \param[in] hpkp_db Handle to an HPKP database
412  * \return 0 if the operation was successful, negative number in case of error.
413  *
414  * Saves the current HPKP database to persistent storage
415  *
416  * In case of databases created by wget_hpkp_db_init(), HPKP entries will be saved into file specified by
417  * \p fname parameter of wget_hpkp_db_init(). In case of failure -1 will be returned with errno set.
418  *
419  * If \p fname is NULL then this function returns -1 and does nothing else.
420  */
wget_hpkp_db_save(wget_hpkp_db * hpkp_db)421 int wget_hpkp_db_save(wget_hpkp_db *hpkp_db)
422 {
423 	if (plugin_vtable)
424 		return plugin_vtable->save(hpkp_db);
425 
426 	if (!hpkp_db)
427 		return -1;
428 
429 	int size;
430 
431 	if (!hpkp_db->fname || !*hpkp_db->fname)
432 		return -1;
433 
434 	if (wget_update_file(hpkp_db->fname,
435 			     (wget_update_load_fn *) hpkp_db_load,
436 			     (wget_update_load_fn *) hpkp_db_save,
437 			     hpkp_db))
438 	{
439 		error_printf(_("Failed to write HPKP file '%s'\n"), hpkp_db->fname);
440 		return -1;
441 	}
442 
443 	if ((size = wget_hashmap_size(hpkp_db->entries)))
444 		debug_printf("Saved %d HPKP entr%s into '%s'\n", size, size != 1 ? "ies" : "y", hpkp_db->fname);
445 	else
446 		debug_printf("No HPKP entries to save. Table is empty.\n");
447 
448 	return 0;
449 }
450 
451 /**
452  * \param[in] hpkp_db Older HPKP database already passed to wget_hpkp_db_deinit(), or NULL
453  * \param[in] fname Name of the file where the data should be stored, or NULL
454  * \return Handle (pointer) to an HPKP database
455  *
456  * Constructor for the default implementation of HSTS database.
457  *
458  * This function does no file IO, data is loaded from file specified by `fname` when wget_hpkp_db_load() is called.
459  * The entries in the file are subject to sanity checks as if they were added to the HPKP database
460  * via wget_hpkp_db_add(). In particular, if an entry is expired due to `creation_time + max_age > cur_time`
461  * it will not be added to the database, and a subsequent call to wget_hpkp_db_save() with the same `hpkp_db_priv`
462  * handle and file name will overwrite the file without all the expired entries.
463  *
464  * Since the format of the file might change without notice, hand-crafted files are discouraged.
465  * To create an HPKP database file that is guaranteed to be correctly parsed by this function,
466  * wget_hpkp_db_save() should be used.
467  *
468  */
wget_hpkp_db_init(wget_hpkp_db * hpkp_db,const char * fname)469 wget_hpkp_db *wget_hpkp_db_init(wget_hpkp_db *hpkp_db, const char *fname)
470 {
471 	if (plugin_vtable)
472 		return plugin_vtable->init(hpkp_db, fname);
473 
474 	if (!hpkp_db)
475 		hpkp_db = wget_calloc(1, sizeof(struct wget_hpkp_db_st));
476 	else
477 		memset(hpkp_db, 0, sizeof(*hpkp_db));
478 
479 	if (fname)
480 		hpkp_db->fname = wget_strdup(fname);
481 	hpkp_db->entries = wget_hashmap_create(16, (wget_hashmap_hash_fn *) hash_hpkp, (wget_hashmap_compare_fn *) compare_hpkp);
482 	wget_hashmap_set_key_destructor(hpkp_db->entries, (wget_hashmap_key_destructor *) wget_hpkp_free);
483 
484 	/*
485 	 * Keys and values for the hashmap are 'hpkp' entries, so value == key.
486 	 * The hash function hashes hostname.
487 	 * The compare function compares hostname.
488 	 *
489 	 * Since the value == key, we just need the value destructor for freeing hashmap entries.
490 	 */
491 
492 	wget_thread_mutex_init(&hpkp_db->mutex);
493 
494 	return (wget_hpkp_db *) hpkp_db;
495 }
496 
497 /**
498  * \param[in] hpkp_db HPKP database created using wget_hpkp_db_init()
499  * \param[in] fname Name of the file where the data should be stored, or NULL
500  *
501  * Changes the file where data should be stored. Works only for databases created by wget_hpkp_db_init().
502  * This function does no file IO, data is loaded when wget_hpkp_db_load() is called.
503  */
wget_hpkp_db_set_fname(wget_hpkp_db * hpkp_db,const char * fname)504 void wget_hpkp_db_set_fname(wget_hpkp_db *hpkp_db, const char *fname)
505 {
506 	xfree(hpkp_db->fname);
507 	hpkp_db->fname = wget_strdup(fname);
508 }
509 
510 /**@}*/
511