From 4ca6b400d152b814c30568d04c504eb4cb09aa72 Mon Sep 17 00:00:00 2001
From: Ethan Welker <code@nuew.net>
Date: Fri, 24 Aug 2018 08:10:22 -0400
Subject: [PATCH] Hash domains in HSTS database to improve privacy

* src/hsts.c: Hash domains in HSTS database to improve privacy
* bootstrap.conf: Add modules crypto/gc and crypto/gc-random

This fulfils the feature request that was originally Debian bug #870813,
and is on Savannah as bug #51666.
---
 bootstrap.conf |   2 +
 src/hsts.c     | 466 ++++++++++++++++++++++++++++---------------------
 2 files changed, 273 insertions(+), 195 deletions(-)

diff --git a/bootstrap.conf b/bootstrap.conf
index bb44dc77..5ee13abf 100644
--- a/bootstrap.conf
+++ b/bootstrap.conf
@@ -72,6 +72,8 @@ mkdir
 mkstemp
 mkostemp
 nanosleep
+crypto/gc
+crypto/gc-random
 crypto/md2
 crypto/md4
 crypto/md5
diff --git a/src/hsts.c b/src/hsts.c
index 64149e4d..521d3dce 100644
--- a/src/hsts.c
+++ b/src/hsts.c
@@ -35,6 +35,8 @@ as that of the covered work.  */
 #include "init.h" /* for home_dir() */
 #include "hash.h"
 #include "c-ctype.h"
+#include "sha256.h"
+#include "gc.h"
 #ifdef TESTING
 #include "../tests/unit-tests.h"
 #endif
@@ -48,24 +50,28 @@ as that of the covered work.  */
 #include <stdio.h>
 #include <sys/file.h>
 
+#define INITIAL_POLICY_CAPACITY 8
+#define HASH_SIZE SHA256_DIGEST_SIZE
+#define SALT_SIZE 16
+
 struct hsts_store {
-  struct hash_table *table;
+  struct hsts_policy *policies;
+  size_t num_policies;
+  size_t max_policies;
   time_t last_mtime;
   bool changed;
 };
 
-struct hsts_kh {
-  char *host;
+struct hsts_policy {
+  char host_hash[HASH_SIZE];
+  char host_salt[SALT_SIZE];
   int explicit_port;
-};
-
-struct hsts_kh_info {
   time_t created;
   time_t max_age;
   bool include_subdomains;
 };
 
-enum hsts_kh_match {
+enum hsts_match {
   NO_MATCH,
   SUPERDOMAIN_MATCH,
   CONGRUENT_MATCH
@@ -81,131 +87,139 @@ enum hsts_kh_match {
 #define MAKE_EXPLICIT_PORT(s, p) (s == SCHEME_HTTPS ? (p == DEFAULT_SSL_PORT ? 0 : p) \
     : (p == DEFAULT_HTTP_PORT ? 0 : p))
 
-/* Hashing and comparison functions for the hash table */
-
-#ifdef __clang__
-__attribute__((no_sanitize("integer")))
-#endif
-static unsigned long
-hsts_hash_func (const void *key)
-{
-  struct hsts_kh *k = (struct hsts_kh *) key;
-  const char *h = NULL;
-  unsigned int hash = k->explicit_port;
-
-  for (h = k->host; *h; h++)
-    hash = hash * 31 + *h;
-
-  return hash;
-}
-
-static int
-hsts_cmp_func (const void *h1, const void *h2)
-{
-  struct hsts_kh *kh1 = (struct hsts_kh *) h1,
-      *kh2 = (struct hsts_kh *) h2;
-
-  return (!strcmp (kh1->host, kh2->host)) && (kh1->explicit_port == kh2->explicit_port);
-}
-
 /* Private functions. Feel free to make some of these public when needed. */
 
-static struct hsts_kh_info *
+static void
+hsts_hash_salt (const char *host, size_t host_len, const char *host_salt, char *out)
+{
+  struct sha256_ctx ctx;
+
+  sha256_init_ctx (&ctx);
+  sha256_process_bytes (host_salt, SALT_SIZE, &ctx);
+  sha256_process_bytes (host, host_len, &ctx);
+  sha256_finish_ctx (&ctx, out);
+}
+
+static bool
+hsts_hash_new_host (const char *host, char *host_hash, char *host_salt)
+{
+  bool success = true;
+  char *host_lower = xstrdup_lower (host);
+
+  if (gc_nonce (host_salt, SALT_SIZE))
+    {
+      success = false;
+      goto bail;
+    }
+
+  hsts_hash_salt (host_lower, strlen(host_lower), host_salt, host_hash);
+
+bail:
+  xfree (host_lower);
+  return success;
+}
+
+static size_t
+hsts_find_entry_raw (hsts_store_t store,
+                     const char *host_hash,
+                     const char *host_salt,
+                     int explicit_port)
+{
+  size_t i;
+
+  for (i = 0; i < store->num_policies; i++)
+    if (explicit_port == store->policies[i].explicit_port &&
+        !memcmp (host_hash, store->policies[i].host_hash, HASH_SIZE))
+      return i;
+
+  return SIZE_MAX;
+}
+
+/* note: this function does NOT canonicalize the host */
+static size_t
+hsts_find_entry_exact (hsts_store_t store,
+                       const char *host, int explicit_port)
+{
+  char host_hash[HASH_SIZE];
+  size_t i, len = strlen(host);
+
+  for (i = 0; i < store->num_policies; i++)
+    {
+      if (explicit_port != store->policies[i].explicit_port)
+        continue;
+
+      hsts_hash_salt (host, len, store->policies[i].host_salt, host_hash);
+      if (!memcmp (host_hash, store->policies[i].host_hash, HASH_SIZE))
+        return i;
+    }
+
+  return SIZE_MAX;
+}
+
+static size_t
 hsts_find_entry (hsts_store_t store,
                  const char *host, int explicit_port,
-                 enum hsts_kh_match *match_type,
-                 struct hsts_kh *kh)
+                 enum hsts_match *match)
 {
-  struct hsts_kh *k = NULL;
-  struct hsts_kh_info *khi = NULL;
-  enum hsts_kh_match match = NO_MATCH;
+  size_t index = SIZE_MAX;
   char *pos = NULL;
   char *org_ptr = NULL;
+  char *host_lower = xstrdup_lower (host);
 
-  k = (struct hsts_kh *) xnew (struct hsts_kh);
-  k->host = xstrdup_lower (host);
-  k->explicit_port = explicit_port;
+  *match = NO_MATCH;
 
   /* save pointer so that we don't get into trouble later when freeing */
-  org_ptr = k->host;
+  org_ptr = host_lower;
 
-  khi = (struct hsts_kh_info *) hash_table_get (store->table, k);
-  if (khi)
-    {
-      match = CONGRUENT_MATCH;
-      goto end;
-    }
-
-  while (match == NO_MATCH &&
-      (pos = strchr (k->host, '.')) && pos - k->host > 0 &&
-      strchr (pos + 1, '.'))
-    {
-      k->host += (pos - k->host + 1);
-      khi = (struct hsts_kh_info *) hash_table_get (store->table, k);
-      if (khi)
-        match = SUPERDOMAIN_MATCH;
-    }
-
-end:
-  /* restore pointer or we'll get a SEGV */
-  k->host = org_ptr;
-
-  /* copy parameters to previous frame */
-  if (match_type)
-    *match_type = match;
-  if (kh)
-    memcpy (kh, k, sizeof (struct hsts_kh));
+  if ((index = hsts_find_entry_exact (store, host_lower, explicit_port)) != SIZE_MAX)
+    *match = CONGRUENT_MATCH;
   else
-    xfree (k->host);
+    while ((pos = strchr (host_lower, '.')) && pos - host_lower > 0 &&
+        strchr (pos + 1, '.'))
+      {
+        host_lower += (pos - host_lower + 1);
+        if ((index = hsts_find_entry_exact (store, host_lower, explicit_port)) != SIZE_MAX)
+          {
+            *match = SUPERDOMAIN_MATCH;
+            break;
+          }
+      }
 
-  xfree (k);
-  return khi;
+  xfree (org_ptr);
+  return index;
 }
 
 static bool
 hsts_new_entry_internal (hsts_store_t store,
-                         const char *host, int port,
+                         const char *host_hash, const char *host_salt, int explicit_port,
                          time_t created, time_t max_age,
-                         bool include_subdomains,
-                         bool check_validity,
-                         bool check_expired,
-                         bool check_duplicates)
+                         bool include_subdomains)
 {
-  struct hsts_kh *kh = xnew (struct hsts_kh);
-  struct hsts_kh_info *khi = xnew0 (struct hsts_kh_info);
-  bool success = false;
+  struct hsts_policy *policy;
 
-  kh->host = xstrdup_lower (host);
-  kh->explicit_port = MAKE_EXPLICIT_PORT (SCHEME_HTTPS, port);
+  if ((created + max_age) < created)
+    return false;
 
-  khi->created = created;
-  khi->max_age = max_age;
-  khi->include_subdomains = include_subdomains;
-
-  /* Check validity */
-  if (check_validity && !hsts_is_host_name_valid (host))
-    goto bail;
-
-  if (check_expired && ((khi->created + khi->max_age) < khi->created))
-    goto bail;
-
-  if (check_duplicates && hash_table_contains (store->table, kh))
-    goto bail;
-
-  /* Now store the new entry */
-  hash_table_put (store->table, kh, khi);
-  success = true;
-
-bail:
-  if (!success)
+  /* (re)alloc policies buffer if necessary */
+  if ((store->num_policies + 1) > store->max_policies)
     {
-      /* abort! */
-      xfree (kh->host);
-      xfree (kh);
-      xfree (khi);
+      store->max_policies = (store->max_policies != 0) ?
+          store->max_policies * 2 :
+          INITIAL_POLICY_CAPACITY;
+      store->policies = xrealloc (store->policies,
+                                  store->max_policies * sizeof(struct hsts_policy));
     }
 
-  return success;
+  policy = &store->policies[store->num_policies++];
+
+  memcpy (policy->host_hash, host_hash, HASH_SIZE);
+  memcpy (policy->host_salt, host_salt, SALT_SIZE);
+  policy->explicit_port = explicit_port;
+  policy->created = created;
+  policy->max_age = max_age;
+  policy->include_subdomains = include_subdomains;
+
+  return true;
 }
 
 /*
@@ -214,56 +228,72 @@ bail:
  */
 static bool
 hsts_add_entry (hsts_store_t store,
-                const char *host, int port,
+                const char *host, int explicit_port,
                 time_t max_age, bool include_subdomains)
 {
+  char host_hash[HASH_SIZE], host_salt[SALT_SIZE];
   time_t t = time (NULL);
 
+  hsts_hash_new_host (host, host_hash, host_salt);
+
   /* It might happen time() returned -1 */
   return (t < 0 ?
       false :
-      hsts_new_entry_internal (store, host, port, t, max_age, include_subdomains, false, true, false));
+      hsts_new_entry_internal (store, host_hash, host_salt, explicit_port,
+                               t, max_age, include_subdomains));
 }
 
 /* Creates a new entry, unless an identical one already exists. */
 static bool
 hsts_new_entry (hsts_store_t store,
-                const char *host, int port,
+                const char *host_hash, const char *host_salt, int explicit_port,
                 time_t created, time_t max_age,
                 bool include_subdomains)
 {
-  return hsts_new_entry_internal (store, host, port, created, max_age, include_subdomains, true, true, true);
+  return (hsts_find_entry_raw (store, host_hash, host_salt, explicit_port) != SIZE_MAX ?
+      false :
+      hsts_new_entry_internal (store, host_hash, host_salt, explicit_port,
+                               created, max_age, include_subdomains));
 }
 
 static void
-hsts_remove_entry (hsts_store_t store, struct hsts_kh *kh)
+hsts_remove_entry (hsts_store_t store, size_t index)
 {
-  hash_table_remove (store->table, kh);
+  memcpy (&store->policies[index], &store->policies[--store->num_policies],
+          sizeof(struct hsts_policy));
+  store->changed = true;
 }
 
 static bool
 hsts_store_merge (hsts_store_t store,
-                  const char *host, int port,
+                  const char *host_hash, const char *host_salt, int explicit_port,
                   time_t created, time_t max_age,
                   bool include_subdomains)
 {
-  enum hsts_kh_match match_type = NO_MATCH;
-  struct hsts_kh_info *khi = NULL;
+  size_t policy_index = SIZE_MAX;
+  struct hsts_policy *policy = NULL;
   bool success = false;
 
-  port = MAKE_EXPLICIT_PORT (SCHEME_HTTPS, port);
-  khi = hsts_find_entry (store, host, port, &match_type, NULL);
-  if (khi && match_type == CONGRUENT_MATCH && created > khi->created)
+  policy_index = hsts_find_entry_raw (store, host_hash, host_salt, explicit_port);
+  if (policy_index != SIZE_MAX && created > policy->created)
     {
+      policy = &store->policies[policy_index];
+
       /* update the entry with the new info */
-      khi->created = created;
-      khi->max_age = max_age;
-      khi->include_subdomains = include_subdomains;
+      policy->created = created;
+      policy->max_age = max_age;
+      policy->include_subdomains = include_subdomains;
 
       success = true;
     }
-  else if (!khi)
-    success = hsts_new_entry (store, host, port, created, max_age, include_subdomains);
+  else if (policy_index == SIZE_MAX)
+    success = hsts_new_entry (store,
+                              host_hash,
+                              host_salt,
+                              explicit_port,
+                              created,
+                              max_age,
+                              include_subdomains);
 
   return success;
 }
@@ -275,9 +305,14 @@ hsts_read_database (hsts_store_t store, FILE *fp, bool merge_with_existing_entri
   size_t len = 0;
   int items_read;
   bool result = false;
-  bool (*func)(hsts_store_t, const char *, int, time_t, time_t, bool);
+  bool (*func)(hsts_store_t, const char *, const char *, int, time_t, time_t, bool);
 
   char host[256];
+  char host_hash[HASH_SIZE], host_salt[SALT_SIZE];
+  char host_hash_b64[BASE64_LENGTH(HASH_SIZE) + 1];
+  char host_salt_b64[BASE64_LENGTH(SALT_SIZE) + 1];
+  size_t host_hash_len, host_salt_len;
+
   int port;
   time_t created, max_age;
   int include_subdomains;
@@ -300,7 +335,43 @@ hsts_read_database (hsts_store_t store, FILE *fp, bool merge_with_existing_entri
                            (unsigned long *) &max_age);
 
       if (items_read == 5)
-        func (store, host, port, created, max_age, !!include_subdomains);
+        {
+          port = MAKE_EXPLICIT_PORT (SCHEME_HTTPS, port);
+          
+          if (host[0] == '|')
+            {
+              items_read = sscanf (host, "|5|%24s|%44s", host_salt_b64, host_hash_b64);
+
+              if (items_read != 2)
+                continue;
+
+              host_hash_len = strlen(host_hash_b64);
+              host_salt_len = strlen(host_salt_b64);
+
+              if (host_hash_len != BASE64_LENGTH(HASH_SIZE) ||
+                  host_salt_len != BASE64_LENGTH(SALT_SIZE))
+                continue;
+
+              host_hash_len = wget_base64_decode(host_hash_b64, host_hash, host_hash_len);
+              host_salt_len = wget_base64_decode(host_salt_b64, host_salt, host_salt_len);
+
+              if (host_hash_len != HASH_SIZE ||
+                  host_salt_len != SALT_SIZE)
+                continue;
+            }
+          /* convert unhashed hosts to hashed hosts */
+          else if (hsts_is_host_name_valid (host))
+            {
+              if (hsts_find_entry_exact (store, host, port) != SIZE_MAX)
+                continue;
+
+              hsts_hash_new_host (host, host_hash, host_salt);
+            }
+          else
+            continue;
+
+          func (store, host_hash, host_salt, port, created, max_age, !!include_subdomains);
+        }
     }
 
   xfree (line);
@@ -312,23 +383,30 @@ hsts_read_database (hsts_store_t store, FILE *fp, bool merge_with_existing_entri
 static void
 hsts_store_dump (hsts_store_t store, FILE *fp)
 {
-  hash_table_iterator it;
+  size_t i;
+  char host_hash[BASE64_LENGTH(HASH_SIZE) + 1];
+  char host_salt[BASE64_LENGTH(SALT_SIZE) + 1];
+  struct hsts_policy *policy = NULL;
 
   /* Print preliminary comments. We don't care if any of these fail. */
   fputs ("# HSTS 1.0 Known Hosts database for GNU Wget.\n", fp);
   fputs ("# Edit at your own risk.\n", fp);
-  fputs ("# <hostname>\t<port>\t<incl. subdomains>\t<created>\t<max-age>\n", fp);
+  fputs ("# <hashed hostname>\t<port>\t<incl. subdomains>\t<created>\t<max-age>\n", fp);
 
   /* Now cycle through the HSTS store in memory and dump the entries */
-  for (hash_table_iterate (store->table, &it); hash_table_iter_next (&it);)
+  for (i = 0; i < store->num_policies; i++)
     {
-      struct hsts_kh *kh = (struct hsts_kh *) it.key;
-      struct hsts_kh_info *khi = (struct hsts_kh_info *) it.value;
+      policy = &store->policies[i];
 
-      if (fprintf (fp, "%s\t%d\t%d\t%lu\t%lu\n",
-                   kh->host, kh->explicit_port, khi->include_subdomains,
-                   (unsigned long) khi->created,
-                   (unsigned long) khi->max_age) < 0)
+      wget_base64_encode(policy->host_hash, HASH_SIZE, host_hash);
+      wget_base64_encode(policy->host_salt, SALT_SIZE, host_salt);
+
+      if (fprintf (fp, "|5|%s|%s\t%d\t%d\t%lu\t%lu\n",
+                   host_salt, host_hash,
+                   policy->explicit_port,
+                   policy->include_subdomains,
+                   (unsigned long) policy->created,
+                   (unsigned long) policy->max_age) < 0)
         {
           logprintf (LOG_ALWAYS, "Could not write the HSTS database correctly.\n");
           break;
@@ -374,21 +452,22 @@ bool
 hsts_match (hsts_store_t store, struct url *u)
 {
   bool url_changed = false;
-  struct hsts_kh_info *entry = NULL;
-  struct hsts_kh *kh = xnew(struct hsts_kh);
-  enum hsts_kh_match match = NO_MATCH;
+  size_t policy_index = SIZE_MAX;
+  struct hsts_policy *policy = NULL;
+  enum hsts_match match = NO_MATCH;
   int port = MAKE_EXPLICIT_PORT (u->scheme, u->port);
 
   /* avoid doing any computation if we're already in HTTPS */
   if (!hsts_is_scheme_valid (u->scheme))
     {
-      entry = hsts_find_entry (store, u->host, port, &match, kh);
-      if (entry)
+      policy_index = hsts_find_entry (store, u->host, port, &match);
+      if (policy_index != SIZE_MAX)
         {
-          if ((entry->created + entry->max_age) >= time(NULL))
+          policy = &store->policies[policy_index];
+          if ((policy->created + policy->max_age) >= time(NULL))
             {
               if ((match == CONGRUENT_MATCH) ||
-                  (match == SUPERDOMAIN_MATCH && entry->include_subdomains))
+                  (match == SUPERDOMAIN_MATCH && policy->include_subdomains))
                 {
                   /* we found a matching Known HSTS Host
                      rewrite the URL */
@@ -400,16 +479,10 @@ hsts_match (hsts_store_t store, struct url *u)
                 }
             }
           else
-            {
-              hsts_remove_entry (store, kh);
-              store->changed = true;
-            }
+            hsts_remove_entry (store, policy_index);
         }
-      xfree (kh->host);
     }
 
-  xfree (kh);
-
   return url_changed;
 }
 
@@ -441,21 +514,20 @@ hsts_store_entry (hsts_store_t store,
                   time_t max_age, bool include_subdomains)
 {
   bool result = false;
-  enum hsts_kh_match match = NO_MATCH;
-  struct hsts_kh *kh = xnew(struct hsts_kh);
-  struct hsts_kh_info *entry = NULL;
+  enum hsts_match match = NO_MATCH;
+  size_t policy_index = SIZE_MAX;
+  struct hsts_policy *policy = NULL;
 
   if (hsts_is_host_eligible (scheme, host))
     {
       port = MAKE_EXPLICIT_PORT (scheme, port);
-      entry = hsts_find_entry (store, host, port, &match, kh);
-      if (entry && match == CONGRUENT_MATCH)
+      policy_index = hsts_find_entry (store, host, port, &match);
+      if (policy_index != SIZE_MAX && match == CONGRUENT_MATCH)
         {
+          policy = &store->policies[policy_index];
+
           if (max_age == 0)
-            {
-              hsts_remove_entry (store, kh);
-              store->changed = true;
-            }
+            hsts_remove_entry (store, policy_index);
           else if (max_age > 0)
             {
               /* RFC 6797 states that 'max_age' is a TTL relative to the
@@ -465,17 +537,17 @@ hsts_store_entry (hsts_store_t store,
                * See also Section 11.2. */
               time_t t = time (NULL);
 
-              if (t != -1 && t != entry->created)
+              if (t != -1 && t != policy->created)
                 {
-                  entry->created = t;
-                  entry->max_age = max_age;
-                  entry->include_subdomains = include_subdomains;
+                  policy->created = t;
+                  policy->max_age = max_age;
+                  policy->include_subdomains = include_subdomains;
                   store->changed = true;
                 }
             }
           /* we ignore negative max_ages */
         }
-      else if (entry == NULL || match == SUPERDOMAIN_MATCH)
+      else if (policy_index == SIZE_MAX || match == SUPERDOMAIN_MATCH)
         {
           /* Either we didn't find a matching host,
              or we got a superdomain match.
@@ -489,11 +561,8 @@ hsts_store_entry (hsts_store_t store,
             store->changed = true;
         }
       /* we ignore new entries with max_age == 0 */
-      xfree (kh->host);
     }
 
-  xfree (kh);
-
   return result;
 }
 
@@ -503,8 +572,12 @@ hsts_store_open (const char *filename)
   hsts_store_t store = NULL;
   file_stats_t fstats;
 
-  store = xnew0 (struct hsts_store);
-  store->table = hash_table_new (0, hsts_hash_func, hsts_cmp_func);
+  gc_init ();
+
+  store = xnew (struct hsts_store);
+  store->policies = NULL;
+  store->num_policies = 0;
+  store->max_policies = 0;
   store->last_mtime = 0;
   store->changed = false;
 
@@ -555,7 +628,7 @@ hsts_store_save (hsts_store_t store, const char *filename)
   FILE *fp = NULL;
   int fd = 0;
 
-  if (filename && hash_table_count (store->table) > 0)
+  if (filename && store->num_policies > 0)
     {
       fp = fopen (filename, "a+");
       if (fp)
@@ -594,17 +667,8 @@ hsts_store_has_changed (hsts_store_t store)
 void
 hsts_store_close (hsts_store_t store)
 {
-  hash_table_iterator it;
-
-  /* free all the host fields */
-  for (hash_table_iterate (store->table, &it); hash_table_iter_next (&it);)
-    {
-      xfree (((struct hsts_kh *) it.key)->host);
-      xfree (it.key);
-      xfree (it.value);
-    }
-
-  hash_table_destroy (store->table);
+  xfree (store->policies);
+  gc_done ();
 }
 
 #ifdef TESTING
@@ -699,8 +763,8 @@ test_url_rewrite (hsts_store_t s, const char *url, int port, bool rewrite)
 const char *
 test_hsts_new_entry (void)
 {
-  enum hsts_kh_match match = NO_MATCH;
-  struct hsts_kh_info *khi;
+  enum hsts_match match = NO_MATCH;
+  struct hsts_policy *policy;
   hsts_store_t s;
   bool created;
 
@@ -713,28 +777,28 @@ test_hsts_new_entry (void)
   created = hsts_store_entry (s, SCHEME_HTTPS, "www.foo.com", 443, 1234, true);
   mu_assert("A new entry should have been created", created == true);
 
-  khi = hsts_find_entry (s, "www.foo.com", MAKE_EXPLICIT_PORT (SCHEME_HTTPS, 443), &match, NULL);
+  policy = hsts_find_entry (s, "www.foo.com", MAKE_EXPLICIT_PORT (SCHEME_HTTPS, 443), &match);
   mu_assert("Should've been a congruent match", match == CONGRUENT_MATCH);
-  mu_assert("No valid HSTS info was returned", khi != NULL);
-  mu_assert("Variable 'max_age' should be 1234", khi->max_age == 1234);
-  mu_assert("Variable 'include_subdomains' should be asserted", khi->include_subdomains == true);
+  mu_assert("No valid HSTS info was returned", policy != NULL);
+  mu_assert("Variable 'max_age' should be 1234", policy->max_age == 1234);
+  mu_assert("Variable 'include_subdomains' should be asserted", policy->include_subdomains == true);
 
-  khi = hsts_find_entry (s, "b.www.foo.com", MAKE_EXPLICIT_PORT (SCHEME_HTTPS, 443), &match, NULL);
+  policy = hsts_find_entry (s, "b.www.foo.com", MAKE_EXPLICIT_PORT (SCHEME_HTTPS, 443), &match);
   mu_assert("Should've been a superdomain match", match == SUPERDOMAIN_MATCH);
-  mu_assert("No valid HSTS info was returned", khi != NULL);
-  mu_assert("Variable 'max_age' should be 1234", khi->max_age == 1234);
-  mu_assert("Variable 'include_subdomains' should be asserted", khi->include_subdomains == true);
+  mu_assert("No valid HSTS info was returned", policy != NULL);
+  mu_assert("Variable 'max_age' should be 1234", policy->max_age == 1234);
+  mu_assert("Variable 'include_subdomains' should be asserted", policy->include_subdomains == true);
 
-  khi = hsts_find_entry (s, "ww.foo.com", MAKE_EXPLICIT_PORT (SCHEME_HTTPS, 443), &match, NULL);
+  policy = hsts_find_entry (s, "ww.foo.com", MAKE_EXPLICIT_PORT (SCHEME_HTTPS, 443), &match);
   mu_assert("Should've been no match", match == NO_MATCH);
 
-  khi = hsts_find_entry (s, "foo.com", MAKE_EXPLICIT_PORT (SCHEME_HTTPS, 443), &match, NULL);
+  policy = hsts_find_entry (s, "foo.com", MAKE_EXPLICIT_PORT (SCHEME_HTTPS, 443), &match);
   mu_assert("Should've been no match", match == NO_MATCH);
 
-  khi = hsts_find_entry (s, ".foo.com", MAKE_EXPLICIT_PORT (SCHEME_HTTPS, 443), &match, NULL);
+  policy = hsts_find_entry (s, ".foo.com", MAKE_EXPLICIT_PORT (SCHEME_HTTPS, 443), &match);
   mu_assert("Should've been no match", match == NO_MATCH);
 
-  khi = hsts_find_entry (s, ".www.foo.com", MAKE_EXPLICIT_PORT (SCHEME_HTTPS, 443), &match, NULL);
+  policy = hsts_find_entry (s, ".www.foo.com", MAKE_EXPLICIT_PORT (SCHEME_HTTPS, 443), &match);
   mu_assert("Should've been no match", match == NO_MATCH);
 
   hsts_store_close (s);
@@ -785,6 +849,10 @@ test_hsts_url_rewrite_congruent (void)
   return NULL;
 }
 
+#define FOO_HASH_EXAMPLE_COM "|5|UdG/QM2+O+fkNnrvOQVD5w==|+5d091CjKbq4VnqdOciVBGQGYdblTtMkUqcig+AZp8Q="
+#define BAR_HASH_EXAMPLE_COM "|5|/dZvs5FEAkeHoq6ZJ0W0DA==|rsx42wQ0mO+HQ9QE+veRSd6rVBPe8b/kQXbCw4IBKmo="
+#define TEST_HASH_EXAMPLE_COM "|5|CGtoi3+APJAyZOZONwvDLQ==|W5C9fqGV1h4q6ByTZCo6KRJwLcsmzprsVmr38Ge2Y84="
+
 const char*
 test_hsts_read_database (void)
 {
@@ -803,17 +871,25 @@ test_hsts_read_database (void)
           fprintf (fp, "foo.example.com\t0\t1\t%lu\t123\n",(unsigned long) created);
           fprintf (fp, "bar.example.com\t0\t0\t%lu\t456\n", (unsigned long) created);
           fprintf (fp, "test.example.com\t8080\t0\t%lu\t789\n", (unsigned long) created);
+          fprintf (fp, FOO_HASH_EXAMPLE_COM "\t0\t1\t%lu\t123\n",(unsigned long) created);
+          fprintf (fp, BAR_HASH_EXAMPLE_COM "\t0\t0\t%lu\t456\n", (unsigned long) created);
+          fprintf (fp, TEST_HASH_EXAMPLE_COM "\t8080\t0\t%lu\t789\n", (unsigned long) created);
           fclose (fp);
 
           table = hsts_store_open (file);
 
           TEST_URL_RW (table, "foo.example.com", 80);
+          TEST_URL_RW (table, "foo-hash.example.com", 80);
           TEST_URL_RW (table, "www.foo.example.com", 80);
+          TEST_URL_RW (table, "www.foo-hash.example.com", 80);
           TEST_URL_RW (table, "bar.example.com", 80);
+          TEST_URL_RW (table, "bar-hash.example.com", 80);
 
           TEST_URL_NORW(table, "www.bar.example.com", 80);
+          TEST_URL_NORW(table, "www.bar-hash.example.com", 80);
 
           TEST_URL_RW (table, "test.example.com", 8080);
+          TEST_URL_RW (table, "test-hash.example.com", 8080);
 
           hsts_store_close (table);
           close_hsts_test_store (table);
-- 
2.18.0

