Coverage Report

Created: 2026-09-28 06:47

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