Coverage Report

Created: 2025-01-28 06:58

/src/wget2/libwget/hpkp_db.c
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright (c) 2015-2024 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
57
void wget_hpkp_set_plugin(const wget_hpkp_db_vtable *vtable)
58
0
{
59
0
  plugin_vtable = vtable;
60
0
}
61
62
#ifdef __clang__
63
__attribute__((no_sanitize("integer")))
64
#endif
65
WGET_GCC_PURE
66
static unsigned int hash_hpkp(const wget_hpkp *hpkp)
67
6.87k
{
68
6.87k
  unsigned int hash = 0;
69
6.87k
  const unsigned char *p;
70
71
27.8k
  for (p = (unsigned char *)hpkp->host; *p; p++)
72
20.9k
    hash = hash * 101 + *p; // possible integer overflow, suppression above
73
74
6.87k
  return hash;
75
6.87k
}
76
77
WGET_GCC_NONNULL_ALL WGET_GCC_PURE
78
static int compare_hpkp(const wget_hpkp *h1, const wget_hpkp *h2)
79
1.28k
{
80
1.28k
  return strcmp(h1->host, h2->host);
81
1.28k
}
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
 */
93
void wget_hpkp_db_deinit(wget_hpkp_db *hpkp_db)
94
1.72k
{
95
1.72k
  if (plugin_vtable) {
96
0
    plugin_vtable->deinit(hpkp_db);
97
0
    return;
98
0
  }
99
100
1.72k
  if (hpkp_db) {
101
1.72k
    xfree(hpkp_db->fname);
102
1.72k
    wget_thread_mutex_lock(hpkp_db->mutex);
103
1.72k
    wget_hashmap_free(&hpkp_db->entries);
104
1.72k
    wget_thread_mutex_unlock(hpkp_db->mutex);
105
106
1.72k
    wget_thread_mutex_destroy(&hpkp_db->mutex);
107
1.72k
  }
108
1.72k
}
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
 */
120
void wget_hpkp_db_free(wget_hpkp_db **hpkp_db)
121
11.9k
{
122
11.9k
  if (plugin_vtable) {
123
0
    plugin_vtable->free(hpkp_db);
124
0
    return;
125
0
  }
126
127
11.9k
  if (hpkp_db && *hpkp_db) {
128
1.72k
    wget_hpkp_db_deinit(*hpkp_db);
129
1.72k
    xfree(*hpkp_db);
130
1.72k
  }
131
11.9k
}
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
 */
148
int wget_hpkp_db_check_pubkey(wget_hpkp_db *hpkp_db, const char *host, const void *pubkey, size_t pubkeysize)
149
1.72k
{
150
1.72k
  if (plugin_vtable)
151
0
    return plugin_vtable->check_pubkey(hpkp_db, host, pubkey, pubkeysize);
152
153
1.72k
  wget_hpkp *hpkp = NULL;
154
1.72k
  int subdomain = 0;
155
1.72k
  char digest[32];
156
1.72k
  size_t digestlen = wget_hash_get_len(WGET_DIGTYPE_SHA256);
157
158
1.72k
  if (digestlen > sizeof(digest)) {
159
0
    error_printf(_("%s: Unexpected hash len %zu > %zu\n"), __func__, digestlen, sizeof(digest));
160
0
    return -1;
161
0
  }
162
163
5.14k
  for (const char *domain = host; *domain && !hpkp; domain = strchrnul(domain, '.')) {
164
5.12k
    while (*domain == '.')
165
1.70k
      domain++;
166
167
3.42k
    wget_hpkp key = { .host = domain };
168
169
3.42k
    if (!wget_hashmap_get(hpkp_db->entries, &key, &hpkp))
170
3.07k
      subdomain = 1;
171
3.42k
  }
172
173
1.72k
  if (!hpkp)
174
1.37k
    return 0; // OK, host is not in database
175
176
350
  if (subdomain && !hpkp->include_subdomains)
177
24
    return 0; // OK, found a matching super domain which isn't responsible for <host>
178
179
326
  if (wget_hash_fast(WGET_DIGTYPE_SHA256, pubkey, pubkeysize, digest))
180
0
    return -1;
181
182
326
  wget_hpkp_pin pinkey = { .pin = digest, .pinsize = digestlen, .hash_type = "sha256" };
183
184
326
  if (wget_vector_find(hpkp->pins, &pinkey) != -1)
185
0
    return 1; // OK, pinned pubkey found
186
187
326
  return -2;
188
326
}
189
190
/* We 'consume' _hpkp and thus set *_hpkp to NULL, so that the calling function
191
 * can't access it any more */
192
/**
193
 * \param[in] hpkp_db An HPKP database
194
 * \param[in] hpkp pointer to HPKP database entry (will be set to NULL)
195
 *
196
 * Adds an entry to given HPKP database. The entry will replace any entry with same `host` (see wget_hpkp_set_host()).
197
 * If `maxage` property of `hpkp` is zero, any existing entry with same `host` property will be removed.
198
 *
199
 * The database takes the ownership of the HPKP entry and the calling function must not access the entry afterwards.
200
 *
201
 * This function is thread-safe and can be called from multiple threads concurrently.
202
 * Any implementation for this function must be thread-safe as well.
203
 */
204
void wget_hpkp_db_add(wget_hpkp_db *hpkp_db, wget_hpkp **_hpkp)
205
4.77k
{
206
4.77k
  if (plugin_vtable) {
207
0
    plugin_vtable->add(hpkp_db, _hpkp);
208
0
    *_hpkp = NULL;
209
0
    return;
210
0
  }
211
212
4.77k
  if (!_hpkp || !*_hpkp)
213
2.70k
    return;
214
215
2.07k
  wget_hpkp *hpkp = *_hpkp;
216
217
2.07k
  wget_thread_mutex_lock(hpkp_db->mutex);
218
219
2.07k
  if (hpkp->maxage == 0 || wget_vector_size(hpkp->pins) == 0) {
220
650
    if (wget_hashmap_remove(hpkp_db->entries, hpkp))
221
28
      debug_printf("removed HPKP %s\n", hpkp->host);
222
650
    wget_hpkp_free(hpkp);
223
1.42k
  } else {
224
1.42k
    wget_hpkp *old;
225
226
1.42k
    if (wget_hashmap_get(hpkp_db->entries, hpkp, &old)) {
227
51
      old->created = hpkp->created;
228
51
      old->maxage = hpkp->maxage;
229
51
      old->expires = hpkp->expires;
230
51
      old->include_subdomains = hpkp->include_subdomains;
231
51
      wget_vector_free(&old->pins);
232
51
      old->pins = hpkp->pins;
233
51
      hpkp->pins = NULL;
234
51
      debug_printf("update HPKP %s (maxage=%lld, includeSubDomains=%d)\n", old->host, (long long)old->maxage, old->include_subdomains);
235
51
      wget_hpkp_free(hpkp);
236
1.37k
    } else {
237
      // key and value are the same to make wget_hashmap_get() return old 'hpkp'
238
      /* debug_printf("add HPKP %s (maxage=%lld, includeSubDomains=%d)\n", hpkp->host, (long long)hpkp->maxage, hpkp->include_subdomains); */
239
1.37k
      wget_hashmap_put(hpkp_db->entries, hpkp, hpkp);
240
      // no need to free anything here
241
1.37k
    }
242
1.42k
  }
243
244
2.07k
  wget_thread_mutex_unlock(hpkp_db->mutex);
245
246
2.07k
  *_hpkp = NULL;
247
2.07k
}
248
249
static int hpkp_db_load(wget_hpkp_db *hpkp_db, FILE *fp)
250
1.72k
{
251
1.72k
  int64_t created, max_age;
252
1.72k
  long long _created, _max_age;
253
1.72k
  int include_subdomains;
254
255
1.72k
  wget_hpkp *hpkp = NULL;
256
1.72k
  struct stat st;
257
1.72k
  char *buf = NULL;
258
1.72k
  size_t bufsize = 0;
259
1.72k
  ssize_t buflen;
260
1.72k
  char hash_type[32], host[256], pin_b64[256];
261
1.72k
  int64_t now = time(NULL);
262
263
  // if the database file hasn't changed since the last read
264
  // there's no need to reload
265
266
1.72k
  if (fstat(fileno(fp), &st) == 0) {
267
0
    if (st.st_mtime != hpkp_db->load_time)
268
0
      hpkp_db->load_time = st.st_mtime;
269
0
    else
270
0
      return 0;
271
0
  }
272
273
9.47k
  while ((buflen = wget_getline(&buf, &bufsize, fp)) >= 0) {
274
7.75k
    char *linep = buf;
275
276
7.75k
    while (isspace(*linep)) linep++; // ignore leading whitespace
277
7.75k
    if (!*linep) continue; // skip empty lines
278
279
6.58k
    if (*linep == '#')
280
197
      continue; // skip comments
281
282
    // strip off \r\n
283
6.39k
    while (buflen > 0 && (buf[buflen] == '\n' || buf[buflen] == '\r'))
284
0
      buf[--buflen] = 0;
285
286
6.39k
    if (*linep != '*') {
287
3.05k
      wget_hpkp_db_add(hpkp_db, &hpkp);
288
289
3.05k
      if (sscanf(linep, "%255s %d %lld %lld", host, &include_subdomains, &_created, &_max_age) == 4) {
290
2.60k
        created = _created;
291
2.60k
        max_age = _max_age;
292
2.60k
        if (created < 0 || max_age < 0 || created >= INT64_MAX / 2 || max_age >= INT64_MAX / 2) {
293
346
          max_age = 0; // avoid integer overflow here
294
346
        }
295
2.60k
        int64_t expires = created + max_age;
296
2.60k
        if (max_age && expires >= now) {
297
2.07k
          hpkp = wget_hpkp_new();
298
2.07k
          if (hpkp) {
299
2.07k
            if (!(hpkp->host = wget_strdup(host)))
300
0
              xfree(hpkp);
301
2.07k
            else {
302
2.07k
              hpkp->maxage = max_age;
303
2.07k
              hpkp->created = created;
304
2.07k
              hpkp->expires = expires;
305
2.07k
              hpkp->include_subdomains = include_subdomains != 0;
306
2.07k
            }
307
2.07k
          }
308
2.07k
        } else
309
527
          debug_printf("HPKP: entry '%s' is expired\n", host);
310
2.60k
      } else {
311
455
        error_printf(_("HPKP: could not parse host line '%s'\n"), buf);
312
455
      }
313
3.33k
    } else if (hpkp) {
314
2.94k
      if (sscanf(linep, "*%31s %255s", hash_type, pin_b64) == 2) {
315
2.87k
        wget_hpkp_pin_add(hpkp, hash_type, pin_b64);
316
2.87k
      } else {
317
73
        error_printf(_("HPKP: could not parse pin line '%s'\n"), buf);
318
73
      }
319
2.94k
    } else {
320
392
      debug_printf("HPKP: skipping PIN entry: '%s'\n", buf);
321
392
    }
322
6.39k
  }
323
324
1.72k
  wget_hpkp_db_add(hpkp_db, &hpkp);
325
326
1.72k
  xfree(buf);
327
328
1.72k
  if (ferror(fp)) {
329
0
    hpkp_db->load_time = 0; // reload on next call to this function
330
0
    return -1;
331
0
  }
332
333
1.72k
  return 0;
334
1.72k
}
335
336
/**
337
 * \param[in] hpkp_db Handle to an HPKP database, obtained with wget_hpkp_db_init()
338
 * \return 0 on success, or a negative number on error
339
 *
340
 * Performs all operations necessary to access the HPKP database entries from persistent storage
341
 * using wget_hpkp_db_check_pubkey() for example.
342
 *
343
 * For databases created by wget_hpkp_db_init() data is loaded from `fname` parameter of wget_hpkp_db_init().
344
 * If this function cannot correctly parse the whole file, -1 is returned.
345
 *
346
 * If `hpkp_db` is NULL then this function returns 0 and does nothing else.
347
 */
348
int wget_hpkp_db_load(wget_hpkp_db *hpkp_db)
349
1.72k
{
350
1.72k
  if (plugin_vtable)
351
0
    return plugin_vtable->load(hpkp_db);
352
353
1.72k
  if (!hpkp_db)
354
0
    return 0;
355
356
1.72k
  if (!hpkp_db->fname || !*hpkp_db->fname)
357
0
    return 0;
358
359
1.72k
  if (wget_update_file(hpkp_db->fname, (wget_update_load_fn *) hpkp_db_load, NULL, hpkp_db)) {
360
0
    error_printf(_("Failed to read HPKP data\n"));
361
0
    return -1;
362
1.72k
  } else {
363
1.72k
    debug_printf("Fetched HPKP data from '%s'\n", hpkp_db->fname);
364
1.72k
    return 0;
365
1.72k
  }
366
1.72k
}
367
368
static int hpkp_save_pin(void *_fp, void *_pin)
369
0
{
370
0
  FILE *fp = _fp;
371
0
  wget_hpkp_pin *pin = _pin;
372
373
0
  wget_fprintf(fp, "*%s %s\n", pin->hash_type, pin->pin_b64);
374
375
0
  if (ferror(fp))
376
0
    return -1;
377
378
0
  return 0;
379
0
}
380
381
WGET_GCC_NONNULL_ALL
382
static int hpkp_save(void *_fp, const void *_hpkp, WGET_GCC_UNUSED void *v)
383
0
{
384
0
  FILE *fp = _fp;
385
0
  const wget_hpkp *hpkp = _hpkp;
386
387
0
  if (wget_vector_size(hpkp->pins) == 0)
388
0
    debug_printf("HPKP: drop '%s', no PIN entries\n", hpkp->host);
389
0
  else if (hpkp->expires < time(NULL))
390
0
    debug_printf("HPKP: drop '%s', expired\n", hpkp->host);
391
0
  else {
392
0
    wget_fprintf(fp, "%s %d %lld %lld\n", hpkp->host, hpkp->include_subdomains, (long long) hpkp->created, (long long) hpkp->maxage);
393
394
0
    if (ferror(fp))
395
0
      return -1;
396
397
0
    return wget_vector_browse(hpkp->pins, hpkp_save_pin, fp);
398
0
  }
399
400
0
  return 0;
401
0
}
402
403
static int hpkp_db_save(wget_hpkp_db *hpkp_db, FILE *fp)
404
0
{
405
0
  wget_hashmap *entries = hpkp_db->entries;
406
407
0
  if (wget_hashmap_size(entries) > 0) {
408
0
    fputs("# HPKP 1.0 file\n", fp);
409
0
    fputs("#Generated by libwget " PACKAGE_VERSION ". Edit at your own risk.\n", fp);
410
0
    fputs("#<hostname> <incl. subdomains> <created> <max-age>\n\n", fp);
411
412
0
    if (ferror(fp))
413
0
      return -1;
414
415
0
    return wget_hashmap_browse(entries, hpkp_save, fp);
416
0
  }
417
418
0
  return 0;
419
0
}
420
421
/**
422
 * \param[in] hpkp_db Handle to an HPKP database
423
 * \return 0 if the operation was successful, negative number in case of error.
424
 *
425
 * Saves the current HPKP database to persistent storage
426
 *
427
 * In case of databases created by wget_hpkp_db_init(), HPKP entries will be saved into file specified by
428
 * \p fname parameter of wget_hpkp_db_init(). In case of failure -1 will be returned with errno set.
429
 *
430
 * If \p fname is NULL then this function returns -1 and does nothing else.
431
 */
432
int wget_hpkp_db_save(wget_hpkp_db *hpkp_db)
433
0
{
434
0
  if (plugin_vtable)
435
0
    return plugin_vtable->save(hpkp_db);
436
437
0
  if (!hpkp_db)
438
0
    return -1;
439
440
0
  int size;
441
442
0
  if (!hpkp_db->fname || !*hpkp_db->fname)
443
0
    return -1;
444
445
0
  if (wget_update_file(hpkp_db->fname,
446
0
           (wget_update_load_fn *) hpkp_db_load,
447
0
           (wget_update_load_fn *) hpkp_db_save,
448
0
           hpkp_db))
449
0
  {
450
0
    error_printf(_("Failed to write HPKP file '%s'\n"), hpkp_db->fname);
451
0
    return -1;
452
0
  }
453
454
0
  if ((size = wget_hashmap_size(hpkp_db->entries)))
455
0
    debug_printf("Saved %d HPKP entr%s into '%s'\n", size, size != 1 ? "ies" : "y", hpkp_db->fname);
456
0
  else
457
0
    debug_printf("No HPKP entries to save. Table is empty.\n");
458
459
0
  return 0;
460
0
}
461
462
/**
463
 * \param[in] hpkp_db Older HPKP database already passed to wget_hpkp_db_deinit(), or NULL
464
 * \param[in] fname Name of the file where the data should be stored, or NULL
465
 * \return Handle (pointer) to an HPKP database
466
 *
467
 * Constructor for the default implementation of HSTS database.
468
 *
469
 * This function does no file IO, data is loaded from file specified by `fname` when wget_hpkp_db_load() is called.
470
 * The entries in the file are subject to sanity checks as if they were added to the HPKP database
471
 * via wget_hpkp_db_add(). In particular, if an entry is expired due to `creation_time + max_age > cur_time`
472
 * it will not be added to the database, and a subsequent call to wget_hpkp_db_save() with the same `hpkp_db_priv`
473
 * handle and file name will overwrite the file without all the expired entries.
474
 *
475
 * Since the format of the file might change without notice, hand-crafted files are discouraged.
476
 * To create an HPKP database file that is guaranteed to be correctly parsed by this function,
477
 * wget_hpkp_db_save() should be used.
478
 *
479
 */
480
wget_hpkp_db *wget_hpkp_db_init(wget_hpkp_db *hpkp_db, const char *fname)
481
1.72k
{
482
1.72k
  if (plugin_vtable)
483
0
    return plugin_vtable->init(hpkp_db, fname);
484
485
1.72k
  if (!hpkp_db) {
486
1.72k
    hpkp_db = wget_calloc(1, sizeof(struct wget_hpkp_db_st));
487
1.72k
    if (!hpkp_db)
488
0
      return NULL;
489
1.72k
  } else
490
0
    memset(hpkp_db, 0, sizeof(*hpkp_db));
491
492
1.72k
  if (fname)
493
1.72k
    hpkp_db->fname = wget_strdup(fname);
494
1.72k
  hpkp_db->entries = wget_hashmap_create(16, (wget_hashmap_hash_fn *) hash_hpkp, (wget_hashmap_compare_fn *) compare_hpkp);
495
1.72k
  wget_hashmap_set_key_destructor(hpkp_db->entries, (wget_hashmap_key_destructor *) wget_hpkp_free);
496
497
  /*
498
   * Keys and values for the hashmap are 'hpkp' entries, so value == key.
499
   * The hash function hashes hostname.
500
   * The compare function compares hostname.
501
   *
502
   * Since the value == key, we just need the value destructor for freeing hashmap entries.
503
   */
504
505
1.72k
  wget_thread_mutex_init(&hpkp_db->mutex);
506
507
1.72k
  return hpkp_db;
508
1.72k
}
509
510
/**
511
 * \param[in] hpkp_db HPKP database created using wget_hpkp_db_init()
512
 * \param[in] fname Name of the file where the data should be stored, or NULL
513
 *
514
 * Changes the file where data should be stored. Works only for databases created by wget_hpkp_db_init().
515
 * This function does no file IO, data is loaded when wget_hpkp_db_load() is called.
516
 */
517
void wget_hpkp_db_set_fname(wget_hpkp_db *hpkp_db, const char *fname)
518
0
{
519
0
  xfree(hpkp_db->fname);
520
0
  hpkp_db->fname = wget_strdup(fname);
521
0
}
522
523
/**@}*/