Selaa lähdekoodia

Merge remote-tracking branch 'public/prop227_v2'

Conflicts:
	src/test/test_dir.c
Nick Mathewson 9 vuotta sitten
vanhempi
commit
fac8d40886

+ 5 - 0
changes/prop227

@@ -0,0 +1,5 @@
+  o Minor features (directory system):
+    - Authorities can now vote on the correct digests and latest versions for
+      different software packages. This allows packages that include Tor to use
+      the Tor authority system as a way to get notified of updates and their
+      correct digests. Implements proposal 227. Closes ticket 10395.

+ 6 - 0
doc/tor.1.txt

@@ -1886,6 +1886,12 @@ on the public Tor network.
     multiple times: the values from multiple lines are spliced together. When
     this is set then **VersioningAuthoritativeDirectory** should be set too.
 
+[[RecommendedPackageVersions]] **RecommendedPackageVersions** __PACKAGENAME__ __VERSION__ __URL__ __DIGESTTYPE__**=**__DIGEST__ ::
+    Adds "package" line to the directory authority's vote.  This information
+    is used to vote on the correct URL and digest for the released versions
+    of different Tor-related packages, so that the consensus can certify
+    them.  This line may appear any number of times.
+
 [[RecommendedClientVersions]] **RecommendedClientVersions** __STRING__::
     STRING is a comma-separated list of Tor versions currently believed to be
     safe for clients to use. This information is included in version 2

+ 21 - 4
src/common/container.c

@@ -518,11 +518,13 @@ smartlist_sort(smartlist_t *sl, int (*compare)(const void **a, const void **b))
 
 /** Given a smartlist <b>sl</b> sorted with the function <b>compare</b>,
  * return the most frequent member in the list.  Break ties in favor of
- * later elements.  If the list is empty, return NULL.
+ * later elements.  If the list is empty, return NULL.  If count_out is
+ * non-null, set it to the most frequent member.
  */
 void *
-smartlist_get_most_frequent(const smartlist_t *sl,
-                            int (*compare)(const void **a, const void **b))
+smartlist_get_most_frequent_(const smartlist_t *sl,
+                             int (*compare)(const void **a, const void **b),
+                             int *count_out)
 {
   const void *most_frequent = NULL;
   int most_frequent_count = 0;
@@ -530,8 +532,11 @@ smartlist_get_most_frequent(const smartlist_t *sl,
   const void *cur = NULL;
   int i, count=0;
 
-  if (!sl->num_used)
+  if (!sl->num_used) {
+    if (count_out)
+      *count_out = 0;
     return NULL;
+  }
   for (i = 0; i < sl->num_used; ++i) {
     const void *item = sl->list[i];
     if (cur && 0 == compare(&cur, &item)) {
@@ -549,6 +554,8 @@ smartlist_get_most_frequent(const smartlist_t *sl,
     most_frequent = cur;
     most_frequent_count = count;
   }
+  if (count_out)
+    *count_out = most_frequent_count;
   return (void*)most_frequent;
 }
 
@@ -728,6 +735,16 @@ smartlist_get_most_frequent_string(smartlist_t *sl)
   return smartlist_get_most_frequent(sl, compare_string_ptrs_);
 }
 
+/** Return the most frequent string in the sorted list <b>sl</b>.
+ * If <b>count_out</b> is provided, set <b>count_out</b> to the
+ * number of times that string appears.
+ */
+char *
+smartlist_get_most_frequent_string_(smartlist_t *sl, int *count_out)
+{
+  return smartlist_get_most_frequent_(sl, compare_string_ptrs_, count_out);
+}
+
 /** Remove duplicate strings from a sorted list, and free them with tor_free().
  */
 void

+ 6 - 2
src/common/container.h

@@ -94,8 +94,11 @@ void smartlist_del_keeporder(smartlist_t *sl, int idx);
 void smartlist_insert(smartlist_t *sl, int idx, void *val);
 void smartlist_sort(smartlist_t *sl,
                     int (*compare)(const void **a, const void **b));
-void *smartlist_get_most_frequent(const smartlist_t *sl,
-                    int (*compare)(const void **a, const void **b));
+void *smartlist_get_most_frequent_(const smartlist_t *sl,
+                    int (*compare)(const void **a, const void **b),
+                    int *count_out);
+#define smartlist_get_most_frequent(sl, compare) \
+  smartlist_get_most_frequent_((sl), (compare), NULL)
 void smartlist_uniq(smartlist_t *sl,
                     int (*compare)(const void **a, const void **b),
                     void (*free_fn)(void *elt));
@@ -106,6 +109,7 @@ void smartlist_sort_digests256(smartlist_t *sl);
 void smartlist_sort_pointers(smartlist_t *sl);
 
 char *smartlist_get_most_frequent_string(smartlist_t *sl);
+char *smartlist_get_most_frequent_string_(smartlist_t *sl, int *count_out);
 char *smartlist_get_most_frequent_digest256(smartlist_t *sl);
 
 void smartlist_uniq_strings(smartlist_t *sl);

+ 9 - 0
src/or/config.c

@@ -86,6 +86,7 @@ static config_abbrev_t option_abbrevs_[] = {
   PLURAL(HiddenServiceExcludeNode),
   PLURAL(NumCPU),
   PLURAL(RendNode),
+  PLURAL(RecommendedPackage),
   PLURAL(RendExcludeNode),
   PLURAL(StrictEntryNode),
   PLURAL(StrictExitNode),
@@ -367,6 +368,7 @@ static config_var_t option_vars_[] = {
   V(RecommendedVersions,         LINELIST, NULL),
   V(RecommendedClientVersions,   LINELIST, NULL),
   V(RecommendedServerVersions,   LINELIST, NULL),
+  V(RecommendedPackages,         LINELIST, NULL),
   V(RefuseUnknownExits,          AUTOBOOL, "auto"),
   V(RejectPlaintextPorts,        CSV,      ""),
   V(RelayBandwidthBurst,         MEMUNIT,  "0"),
@@ -2743,6 +2745,13 @@ options_validate(or_options_t *old_options, or_options_t *options,
              "features to be broken in unpredictable ways.");
   }
 
+  for (cl = options->RecommendedPackages; cl; cl = cl->next) {
+    if (! validate_recommended_package_line(cl->value)) {
+      log_warn(LD_CONFIG, "Invalid RecommendedPackage line %s will be ignored",
+               escaped(cl->value));
+    }
+  }
+
   if (options->AuthoritativeDir) {
     if (!options->ContactInfo && !options->TestingTorNetwork)
       REJECT("Authoritative directory servers must set ContactInfo");

+ 2 - 0
src/or/control.c

@@ -2183,6 +2183,8 @@ static const getinfo_item_t getinfo_items[] = {
          "Brief summary of router status by nickname (v2 directory format)."),
   PREFIX("ns/purpose/", networkstatus,
          "Brief summary of router status by purpose (v2 directory format)."),
+  PREFIX("consensus/", networkstatus,
+         "Information about and from the ns consensus."),
   ITEM("network-status", dir,
        "Brief summary of router status (v1 directory format)"),
   ITEM("circuit-status", events, "List of current circuits originating here."),

+ 86 - 0
src/or/dirserv.c

@@ -2511,6 +2511,15 @@ dirserv_generate_networkstatus_vote_obj(crypto_pk_t *private_key,
 
   v3_out->client_versions = client_versions;
   v3_out->server_versions = server_versions;
+  v3_out->package_lines = smartlist_new();
+  {
+    config_line_t *cl;
+    for (cl = get_options()->RecommendedPackages; cl; cl = cl->next) {
+      if (validate_recommended_package_line(cl->value))
+        smartlist_add(v3_out->package_lines, tor_strdup(cl->value));
+    }
+  }
+
   v3_out->known_flags = smartlist_new();
   smartlist_split_string(v3_out->known_flags,
                 "Authority Exit Fast Guard Stable V2Dir Valid",
@@ -3256,6 +3265,83 @@ connection_dirserv_flushed_some(dir_connection_t *conn)
   }
 }
 
+/** Return true iff <b>line</b> is a valid RecommendedPackages line.
+ */
+/*
+  The grammar is:
+
+    "package" SP PACKAGENAME SP VERSION SP URL SP DIGESTS NL
+
+      PACKAGENAME = NONSPACE
+      VERSION = NONSPACE
+      URL = NONSPACE
+      DIGESTS = DIGEST | DIGESTS SP DIGEST
+      DIGEST = DIGESTTYPE "=" DIGESTVAL
+
+      NONSPACE = one or more non-space printing characters
+
+      DIGESTVAL = DIGESTTYPE = one or more non-=, non-" " characters.
+
+      SP = " "
+      NL = a newline
+
+ */
+int
+validate_recommended_package_line(const char *line)
+{
+  const char *cp = line;
+
+#define WORD()                                  \
+  do {                                          \
+    if (*cp == ' ')                             \
+      return 0;                                 \
+    cp = strchr(cp, ' ');                       \
+    if (!cp)                                    \
+      return 0;                                 \
+  } while (0)
+
+  WORD(); /* skip packagename */
+  ++cp;
+  WORD(); /* skip version */
+  ++cp;
+  WORD(); /* Skip URL */
+  ++cp;
+
+  /* Skip digesttype=digestval + */
+  int n_entries = 0;
+  while (1) {
+    const char *start_of_word = cp;
+    const char *end_of_word = strchr(cp, ' ');
+    if (! end_of_word)
+      end_of_word = cp + strlen(cp);
+
+    if (start_of_word == end_of_word)
+      return 0;
+
+    const char *eq = memchr(start_of_word, '=', end_of_word - start_of_word);
+
+    if (!eq)
+      return 0;
+    if (eq == start_of_word)
+      return 0;
+    if (eq == end_of_word - 1)
+      return 0;
+    if (memchr(eq+1, '=', end_of_word - (eq+1)))
+      return 0;
+
+    ++n_entries;
+    if (0 == *end_of_word)
+      break;
+
+    cp = end_of_word + 1;
+  }
+
+  if (n_entries == 0)
+    return 0;
+
+  return 1;
+}
+
 /** Release all storage used by the directory server. */
 void
 dirserv_free_all(void)

+ 2 - 0
src/or/dirserv.h

@@ -104,6 +104,8 @@ void dirserv_free_all(void);
 void cached_dir_decref(cached_dir_t *d);
 cached_dir_t *new_cached_dir(char *s, time_t published);
 
+int validate_recommended_package_line(const char *line);
+
 #ifdef DIRSERV_PRIVATE
 
 /* Put the MAX_MEASUREMENT_AGE #define here so unit tests can see it */

+ 99 - 1
src/or/dirvote.c

@@ -66,6 +66,7 @@ format_networkstatus_vote(crypto_pk_t *private_signing_key,
 {
   smartlist_t *chunks = smartlist_new();
   const char *client_versions = NULL, *server_versions = NULL;
+  char *packages = NULL;
   char fingerprint[FINGERPRINT_LEN+1];
   char digest[DIGEST_LEN];
   uint32_t addr;
@@ -98,6 +99,18 @@ format_networkstatus_vote(crypto_pk_t *private_signing_key,
     server_versions_line = tor_strdup("");
   }
 
+  if (v3_ns->package_lines) {
+    smartlist_t *tmp = smartlist_new();
+    SMARTLIST_FOREACH(v3_ns->package_lines, const char *, p,
+                      if (validate_recommended_package_line(p))
+                        smartlist_add_asprintf(tmp, "package %s\n", p));
+    packages = smartlist_join_strings(tmp, "", 0, NULL);
+    SMARTLIST_FOREACH(tmp, char *, cp, tor_free(cp));
+    smartlist_free(tmp);
+  } else {
+    packages = tor_strdup("");
+  }
+
   {
     char published[ISO_TIME_LEN+1];
     char va[ISO_TIME_LEN+1];
@@ -132,6 +145,7 @@ format_networkstatus_vote(crypto_pk_t *private_signing_key,
                  "valid-until %s\n"
                  "voting-delay %d %d\n"
                  "%s%s" /* versions */
+                 "%s" /* packages */
                  "known-flags %s\n"
                  "flag-thresholds %s\n"
                  "params %s\n"
@@ -143,6 +157,7 @@ format_networkstatus_vote(crypto_pk_t *private_signing_key,
                  v3_ns->vote_seconds, v3_ns->dist_seconds,
                  client_versions_line,
                  server_versions_line,
+                 packages,
                  flags,
                  flag_thresholds,
                  params,
@@ -230,6 +245,7 @@ format_networkstatus_vote(crypto_pk_t *private_signing_key,
  done:
   tor_free(client_versions_line);
   tor_free(server_versions_line);
+  tor_free(packages);
 
   SMARTLIST_FOREACH(chunks, char *, cp, tor_free(cp));
   smartlist_free(chunks);
@@ -1037,6 +1053,7 @@ networkstatus_compute_consensus(smartlist_t *votes,
   const routerstatus_format_type_t rs_format =
     flavor == FLAV_NS ? NS_V3_CONSENSUS : NS_V3_CONSENSUS_MICRODESC;
   char *params = NULL;
+  char *packages = NULL;
   int added_weights = 0;
   tor_assert(flavor == FLAV_NS || flavor == FLAV_MICRODESC);
   tor_assert(total_authorities >= smartlist_len(votes));
@@ -1120,6 +1137,11 @@ networkstatus_compute_consensus(smartlist_t *votes,
                                                       n_versioning_servers);
     client_versions = compute_consensus_versions_list(combined_client_versions,
                                                       n_versioning_clients);
+    if (consensus_method >= MIN_METHOD_FOR_PACKAGE_LINES) {
+      packages = compute_consensus_package_lines(votes);
+    } else {
+      packages = tor_strdup("");
+    }
 
     SMARTLIST_FOREACH(combined_server_versions, char *, cp, tor_free(cp));
     SMARTLIST_FOREACH(combined_client_versions, char *, cp, tor_free(cp));
@@ -1162,10 +1184,13 @@ networkstatus_compute_consensus(smartlist_t *votes,
                  "voting-delay %d %d\n"
                  "client-versions %s\n"
                  "server-versions %s\n"
+                 "%s" /* packages */
                  "known-flags %s\n",
                  va_buf, fu_buf, vu_buf,
                  vote_seconds, dist_seconds,
-                 client_versions, server_versions, flaglist);
+                 client_versions, server_versions,
+                 packages,
+                 flaglist);
 
     tor_free(flaglist);
   }
@@ -1852,6 +1877,7 @@ networkstatus_compute_consensus(smartlist_t *votes,
 
   tor_free(client_versions);
   tor_free(server_versions);
+  tor_free(packages);
   SMARTLIST_FOREACH(flags, char *, cp, tor_free(cp));
   smartlist_free(flags);
   SMARTLIST_FOREACH(chunks, char *, cp, tor_free(cp));
@@ -1860,6 +1886,78 @@ networkstatus_compute_consensus(smartlist_t *votes,
   return result;
 }
 
+/** Given a list of networkstatus_t for each vote, return a newly allocated
+ * string containing the "package" lines for the vote. */
+STATIC char *
+compute_consensus_package_lines(smartlist_t *votes)
+{
+  const int n_votes = smartlist_len(votes);
+
+  /* This will be a map from "packagename version" strings to arrays
+   * of const char *, with the i'th member of the array corresponding to the
+   * package line from the i'th vote.
+   */
+  strmap_t *package_status = strmap_new();
+
+  SMARTLIST_FOREACH_BEGIN(votes, networkstatus_t *, v) {
+    if (! v->package_lines)
+      continue;
+    SMARTLIST_FOREACH_BEGIN(v->package_lines, const char *, line) {
+      if (! validate_recommended_package_line(line))
+        continue;
+
+      /* Skip 'cp' to the second space in the line. */
+      const char *cp = strchr(line, ' ');
+      if (!cp) continue;
+      ++cp;
+      cp = strchr(cp, ' ');
+      if (!cp) continue;
+
+      char *key = tor_strndup(line, cp - line);
+
+      const char **status = strmap_get(package_status, key);
+      if (!status) {
+        status = tor_calloc(n_votes, sizeof(const char *));
+        strmap_set(package_status, key, status);
+      }
+      status[v_sl_idx] = line; /* overwrite old value */
+      tor_free(key);
+    } SMARTLIST_FOREACH_END(line);
+  } SMARTLIST_FOREACH_END(v);
+
+  smartlist_t *entries = smartlist_new(); /* temporary */
+  smartlist_t *result_list = smartlist_new(); /* output */
+  STRMAP_FOREACH(package_status, key, const char **, values) {
+    int i, count=-1;
+    for (i = 0; i < n_votes; ++i) {
+      if (values[i])
+        smartlist_add(entries, (void*) values[i]);
+    }
+    smartlist_sort_strings(entries);
+    int n_voting_for_entry = smartlist_len(entries);
+    const char *most_frequent =
+      smartlist_get_most_frequent_string_(entries, &count);
+
+    if (n_voting_for_entry >= 3 && count > n_voting_for_entry / 2) {
+      smartlist_add_asprintf(result_list, "package %s\n", most_frequent);
+    }
+
+    smartlist_clear(entries);
+
+  } STRMAP_FOREACH_END;
+
+  smartlist_sort_strings(result_list);
+
+  char *result = smartlist_join_strings(result_list, "", 0, NULL);
+
+  SMARTLIST_FOREACH(result_list, char *, cp, tor_free(cp));
+  smartlist_free(result_list);
+  smartlist_free(entries);
+  strmap_free(package_status, tor_free_);
+
+  return result;
+}
+
 /** Given a consensus vote <b>target</b> and a set of detached signatures in
  * <b>sigs</b> that correspond to the same consensus, check whether there are
  * any new signatures in <b>src_voter_list</b> that should be added to

+ 5 - 1
src/or/dirvote.h

@@ -55,7 +55,7 @@
 #define MIN_SUPPORTED_CONSENSUS_METHOD 13
 
 /** The highest consensus method that we currently support. */
-#define MAX_SUPPORTED_CONSENSUS_METHOD 18
+#define MAX_SUPPORTED_CONSENSUS_METHOD 19
 
 /** Lowest consensus method where microdesc consensuses omit any entry
  * with no microdesc. */
@@ -79,6 +79,9 @@
  * microdescriptors. */
 #define MIN_METHOD_FOR_ID_HASH_IN_MD 18
 
+/** Lowest consensus method where we include "package" lines*/
+#define MIN_METHOD_FOR_PACKAGE_LINES 19
+
 /** Default bandwidth to clip unmeasured bandwidths to using method >=
  * MIN_METHOD_TO_CLIP_UNMEASURED_BW */
 #define DEFAULT_MAX_UNMEASURED_BW_KB 20
@@ -160,6 +163,7 @@ STATIC char *format_networkstatus_vote(crypto_pk_t *private_key,
                                  networkstatus_t *v3_ns);
 STATIC char *dirvote_compute_params(smartlist_t *votes, int method,
                              int total_authorities);
+STATIC char *compute_consensus_package_lines(smartlist_t *votes);
 #endif
 
 #endif

+ 31 - 0
src/or/networkstatus.c

@@ -257,6 +257,10 @@ networkstatus_vote_free(networkstatus_t *ns)
     SMARTLIST_FOREACH(ns->supported_methods, char *, c, tor_free(c));
     smartlist_free(ns->supported_methods);
   }
+  if (ns->package_lines) {
+    SMARTLIST_FOREACH(ns->package_lines, char *, c, tor_free(c));
+    smartlist_free(ns->package_lines);
+  }
   if (ns->voters) {
     SMARTLIST_FOREACH_BEGIN(ns->voters, networkstatus_voter_info_t *, voter) {
       tor_free(voter->nickname);
@@ -1909,6 +1913,33 @@ getinfo_helper_networkstatus(control_connection_t *conn,
   } else if (!strcmpstart(question, "ns/purpose/")) {
     *answer = networkstatus_getinfo_by_purpose(question+11, time(NULL));
     return *answer ? 0 : -1;
+  } else if (!strcmp(question, "consensus/packages")) {
+    const networkstatus_t *ns = networkstatus_get_latest_consensus();
+    if (ns && ns->package_lines)
+      *answer = smartlist_join_strings(ns->package_lines, "\n", 0, NULL);
+    else
+      *errmsg = "No consensus available";
+    return *answer ? 0 : -1;
+  } else if (!strcmp(question, "consensus/valid-after") ||
+             !strcmp(question, "consensus/fresh-until") ||
+             !strcmp(question, "consensus/valid-until")) {
+    const networkstatus_t *ns = networkstatus_get_latest_consensus();
+    if (ns) {
+      time_t t;
+      if (!strcmp(question, "consensus/valid-after"))
+        t = ns->valid_after;
+      else if (!strcmp(question, "consensus/fresh-until"))
+        t = ns->fresh_until;
+      else
+        t = ns->valid_until;
+
+      char tbuf[ISO_TIME_LEN+1];
+      format_iso_time(tbuf, t);
+      *answer = tor_strdup(tbuf);
+    } else {
+      *errmsg = "No consensus available";
+    }
+    return *answer ? 0 : -1;
   } else {
     return 0;
   }

+ 4 - 0
src/or/or.h

@@ -2417,6 +2417,9 @@ typedef struct networkstatus_t {
   /** Vote only: what methods is this voter willing to use? */
   smartlist_t *supported_methods;
 
+  /** List of 'package' lines describing hashes of downloadable packages */
+  smartlist_t *package_lines;
+
   /** How long does this vote/consensus claim that authorities take to
    * distribute their votes to one another? */
   int vote_seconds;
@@ -3433,6 +3436,7 @@ typedef struct {
   config_line_t *RecommendedVersions;
   config_line_t *RecommendedClientVersions;
   config_line_t *RecommendedServerVersions;
+  config_line_t *RecommendedPackages;
   /** Whether dirservers allow router descriptors with private IPs. */
   int DirAllowPrivateAddresses;
   /** Whether routers accept EXTEND cells to routers with private IPs. */

+ 12 - 0
src/or/routerparse.c

@@ -131,6 +131,7 @@ typedef enum {
   K_CONSENSUS_METHOD,
   K_LEGACY_DIR_KEY,
   K_DIRECTORY_FOOTER,
+  K_PACKAGE,
 
   A_PURPOSE,
   A_LAST_LISTED,
@@ -420,6 +421,7 @@ static token_rule_t networkstatus_token_table[] = {
   T1("known-flags",            K_KNOWN_FLAGS,      ARGS,        NO_OBJ ),
   T01("params",                K_PARAMS,           ARGS,        NO_OBJ ),
   T( "fingerprint",            K_FINGERPRINT,      CONCAT_ARGS, NO_OBJ ),
+  T0N("package",               K_PACKAGE,          CONCAT_ARGS, NO_OBJ ),
 
   CERTIFICATE_MEMBERS
 
@@ -2626,6 +2628,16 @@ networkstatus_parse_vote_from_string(const char *s, const char **eos_out,
     ns->server_versions = tor_strdup(tok->args[0]);
   }
 
+  {
+    smartlist_t *package_lst = find_all_by_keyword(tokens, K_PACKAGE);
+    ns->package_lines = smartlist_new();
+    if (package_lst) {
+      SMARTLIST_FOREACH(package_lst, directory_token_t *, t,
+                    smartlist_add(ns->package_lines, tor_strdup(t->args[0])));
+    }
+    smartlist_free(package_lst);
+  }
+
   tok = find_by_keyword(tokens, K_KNOWN_FLAGS);
   ns->known_flags = smartlist_new();
   inorder = 1;

+ 147 - 0
src/test/test_dir.c

@@ -2954,6 +2954,152 @@ test_dir_fetch_type(void *arg)
  done: ;
 }
 
+static void
+test_dir_packages(void *arg)
+{
+  smartlist_t *votes = smartlist_new();
+  char *res = NULL;
+  (void)arg;
+
+#define BAD(s) \
+  tt_int_op(0, ==, validate_recommended_package_line(s));
+#define GOOD(s) \
+  tt_int_op(1, ==, validate_recommended_package_line(s));
+  GOOD("tor 0.2.6.3-alpha "
+       "http://torproject.example.com/dist/tor-0.2.6.3-alpha.tar.gz "
+       "sha256=sssdlkfjdsklfjdskfljasdklfj");
+  GOOD("tor 0.2.6.3-alpha "
+       "http://torproject.example.com/dist/tor-0.2.6.3-alpha.tar.gz "
+       "sha256=sssdlkfjdsklfjdskfljasdklfj blake2b=fred");
+  BAD("tor 0.2.6.3-alpha "
+       "http://torproject.example.com/dist/tor-0.2.6.3-alpha.tar.gz "
+       "sha256=sssdlkfjdsklfjdskfljasdklfj=");
+  BAD("tor 0.2.6.3-alpha "
+       "http://torproject.example.com/dist/tor-0.2.6.3-alpha.tar.gz "
+       "sha256=sssdlkfjdsklfjdskfljasdklfj blake2b");
+  BAD("tor 0.2.6.3-alpha "
+       "http://torproject.example.com/dist/tor-0.2.6.3-alpha.tar.gz ");
+  BAD("tor 0.2.6.3-alpha "
+       "http://torproject.example.com/dist/tor-0.2.6.3-alpha.tar.gz");
+  BAD("tor 0.2.6.3-alpha ");
+  BAD("tor 0.2.6.3-alpha");
+  BAD("tor ");
+  BAD("tor");
+  BAD("");
+  BAD("=foobar sha256="
+      "3c179f46ca77069a6a0bac70212a9b3b838b2f66129cb52d568837fc79d8fcc7");
+  BAD("= = sha256="
+      "3c179f46ca77069a6a0bac70212a9b3b838b2f66129cb52d568837fc79d8fcc7");
+
+  BAD("sha512= sha256="
+      "3c179f46ca77069a6a0bac70212a9b3b838b2f66129cb52d568837fc79d8fcc7");
+
+  votes = smartlist_new();
+  smartlist_add(votes, tor_malloc_zero(sizeof(networkstatus_t)));
+  smartlist_add(votes, tor_malloc_zero(sizeof(networkstatus_t)));
+  smartlist_add(votes, tor_malloc_zero(sizeof(networkstatus_t)));
+  smartlist_add(votes, tor_malloc_zero(sizeof(networkstatus_t)));
+  smartlist_add(votes, tor_malloc_zero(sizeof(networkstatus_t)));
+  smartlist_add(votes, tor_malloc_zero(sizeof(networkstatus_t)));
+  SMARTLIST_FOREACH(votes, networkstatus_t *, ns,
+                    ns->package_lines = smartlist_new());
+
+#define ADD(i, s)                                                       \
+  smartlist_add(((networkstatus_t*)smartlist_get(votes, (i)))->package_lines, \
+                (void*)(s));
+
+  /* Only one vote for this one. */
+  ADD(4, "cisco 99z http://foobar.example.com/ sha256=blahblah");
+
+  /* Only two matching entries for this one, but 3 voters */
+  ADD(1, "mystic 99y http://barfoo.example.com/ sha256=blahblah");
+  ADD(3, "mystic 99y http://foobar.example.com/ sha256=blahblah");
+  ADD(4, "mystic 99y http://foobar.example.com/ sha256=blahblah");
+
+  /* Only two matching entries for this one, but at least 4 voters */
+  ADD(1, "mystic 99p http://barfoo.example.com/ sha256=ggggggg");
+  ADD(3, "mystic 99p http://foobar.example.com/ sha256=blahblah");
+  ADD(4, "mystic 99p http://foobar.example.com/ sha256=blahblah");
+  ADD(5, "mystic 99p http://foobar.example.com/ sha256=ggggggg");
+
+  /* This one has only invalid votes. */
+  ADD(0, "haffenreffer 1.2 http://foobar.example.com/ sha256");
+  ADD(1, "haffenreffer 1.2 http://foobar.example.com/ ");
+  ADD(2, "haffenreffer 1.2 ");
+  ADD(3, "haffenreffer ");
+  ADD(4, "haffenreffer");
+
+  /* Three matching votes for this; it should actually go in! */
+  ADD(2, "element 0.66.1 http://quux.example.com/ sha256=abcdef");
+  ADD(3, "element 0.66.1 http://quux.example.com/ sha256=abcdef");
+  ADD(4, "element 0.66.1 http://quux.example.com/ sha256=abcdef");
+  ADD(1, "element 0.66.1 http://quum.example.com/ sha256=abcdef");
+  ADD(0, "element 0.66.1 http://quux.example.com/ sha256=abcde");
+
+  /* Three votes for A, three votes for B */
+  ADD(0, "clownshoes 22alpha1 http://quumble.example.com/ blake2=foob");
+  ADD(1, "clownshoes 22alpha1 http://quumble.example.com/ blake2=foob");
+  ADD(2, "clownshoes 22alpha1 http://quumble.example.com/ blake2=foob");
+  ADD(3, "clownshoes 22alpha1 http://quumble.example.com/ blake2=fooz");
+  ADD(4, "clownshoes 22alpha1 http://quumble.example.com/ blake2=fooz");
+  ADD(5, "clownshoes 22alpha1 http://quumble.example.com/ blake2=fooz");
+
+  /* Three votes for A, two votes for B */
+  ADD(1, "clownshoes 22alpha3 http://quumble.example.com/ blake2=foob");
+  ADD(2, "clownshoes 22alpha3 http://quumble.example.com/ blake2=foob");
+  ADD(3, "clownshoes 22alpha3 http://quumble.example.com/ blake2=fooz");
+  ADD(4, "clownshoes 22alpha3 http://quumble.example.com/ blake2=fooz");
+  ADD(5, "clownshoes 22alpha3 http://quumble.example.com/ blake2=fooz");
+
+  /* Four votes for A, two for B. */
+  ADD(0, "clownshoes 22alpha4 http://quumble.example.com/ blake2=foob");
+  ADD(1, "clownshoes 22alpha4 http://quumble.example.com/ blake2=foob");
+  ADD(2, "clownshoes 22alpha4 http://quumble.example.cam/ blake2=fooa");
+  ADD(3, "clownshoes 22alpha4 http://quumble.example.cam/ blake2=fooa");
+  ADD(4, "clownshoes 22alpha4 http://quumble.example.cam/ blake2=fooa");
+  ADD(5, "clownshoes 22alpha4 http://quumble.example.cam/ blake2=fooa");
+
+  /* Five votes for A ... all from the same guy.  Three for B. */
+  ADD(0, "cbc 99.1.11.1.1 http://example.com/cbc/ cubehash=ahooy sha512=m");
+  ADD(1, "cbc 99.1.11.1.1 http://example.com/cbc/ cubehash=ahooy sha512=m");
+  ADD(3, "cbc 99.1.11.1.1 http://example.com/cbc/ cubehash=ahooy sha512=m");
+  ADD(2, "cbc 99.1.11.1.1 http://example.com/ cubehash=ahooy");
+  ADD(2, "cbc 99.1.11.1.1 http://example.com/ cubehash=ahooy");
+  ADD(2, "cbc 99.1.11.1.1 http://example.com/ cubehash=ahooy");
+  ADD(2, "cbc 99.1.11.1.1 http://example.com/ cubehash=ahooy");
+  ADD(2, "cbc 99.1.11.1.1 http://example.com/ cubehash=ahooy");
+
+  /* As above but new replaces old: no two match. */
+  ADD(0, "cbc 99.1.11.1.2 http://example.com/cbc/ cubehash=ahooy sha512=m");
+  ADD(1, "cbc 99.1.11.1.2 http://example.com/cbc/ cubehash=ahooy sha512=m");
+  ADD(1, "cbc 99.1.11.1.2 http://example.com/cbc/x cubehash=ahooy sha512=m");
+  ADD(2, "cbc 99.1.11.1.2 http://example.com/cbc/ cubehash=ahooy sha512=m");
+  ADD(2, "cbc 99.1.11.1.2 http://example.com/ cubehash=ahooy");
+  ADD(2, "cbc 99.1.11.1.2 http://example.com/ cubehash=ahooy");
+  ADD(2, "cbc 99.1.11.1.2 http://example.com/ cubehash=ahooy");
+  ADD(2, "cbc 99.1.11.1.2 http://example.com/ cubehash=ahooy");
+  ADD(2, "cbc 99.1.11.1.2 http://example.com/ cubehash=ahooy");
+
+
+  res = compute_consensus_package_lines(votes);
+  tt_assert(res);
+  tt_str_op(res, ==,
+    "package cbc 99.1.11.1.1 http://example.com/cbc/ cubehash=ahooy sha512=m\n"
+    "package clownshoes 22alpha3 http://quumble.example.com/ blake2=fooz\n"
+    "package clownshoes 22alpha4 http://quumble.example.cam/ blake2=fooa\n"
+    "package element 0.66.1 http://quux.example.com/ sha256=abcdef\n"
+    "package mystic 99y http://foobar.example.com/ sha256=blahblah\n"
+            );
+
+#undef ADD
+#undef BAD
+#undef GOOD
+ done:
+  SMARTLIST_FOREACH(votes, networkstatus_t *, ns,
+                    { smartlist_free(ns->package_lines); tor_free(ns); });
+  tor_free(res);
+}
+
 #define DIR_LEGACY(name)                                                   \
   { #name, test_dir_ ## name , TT_FORK, NULL, NULL }
 
@@ -2983,6 +3129,7 @@ struct testcase_t dir_tests[] = {
   DIR(http_handling, 0),
   DIR(purpose_needs_anonymity, 0),
   DIR(fetch_type, 0),
+  DIR(packages, 0),
   END_OF_TESTCASES
 };