commit 6944d2c299b86b0c11b87b7cab8cda77661fc1fa
parent 2827daa54ad20e8c62039c851d82b95dc9eab971
Author: Nick Mathewson <nickm@torproject.org>
Date: Fri, 7 Mar 2025 01:09:44 +0000
Merge branch 'happy-families' into 'main'
Implement proposal 321 (happy families)
Closes #41009
See merge request tpo/core/tor!857
Diffstat:
31 files changed, 1070 insertions(+), 25 deletions(-)
diff --git a/changes/happy-families b/changes/happy-families
@@ -0,0 +1,17 @@
+ o Major feature (happy families):
+
+ - Clients and relays now support "happy families", a system to
+ simplify relay family operation and improve directory performance.
+ With "happy families", relays in a family shares a secret "family key",
+ which they use to prove their membership in the family.
+ Implements proposal 321; closes ticket 41009.
+
+ Note that until enough clients are upgraded,
+ relay operators will still need to configure MyFamily lists.
+ But once clients no longer depend on those lists,
+ we will be able to remove them entirely,
+ thereby simplifying family operation,
+ and making microdescriptor downloads approximately 80% smaller.
+
+ For more information, see
+ https://community.torproject.org/relay/setup/post-install/family-ids/
diff --git a/changes/happy-families-client b/changes/happy-families-client
@@ -0,0 +1,4 @@
+ o Major features (client):
+ - Clients now respect "happy families" per proposal 321.
+ This feature will eventually allow a much more compact representation
+ for relay families, for a significant savings in directory download size.
diff --git a/doc/man/tor.1.txt b/doc/man/tor.1.txt
@@ -168,6 +168,16 @@ The following options in this section are only recognized on the
make sure that they are owned by the user actually running the Tor
daemon on your system.
+[[opt-keygen-family]] **`--keygen-family`** __basename__::
+ Generate a new family ID key in __basename__`.secret_family_key`.
+ To use this key, install it on every relay in your family.
+ (Put it in the relay's `KeyDirectory`.)
+ Also, store the corresponding family ID in __basename__`.public_family_id`.
+ Then enable the corresponding FamilyID option on your relays.
+ This command overwrites these files if they already exist.
+ See https://community.torproject.org/relay/setup/post-install/family-ids/
+ for more information.
+
**`--passphrase-fd`** __FILEDES__::
File descriptor to read the passphrase from. Note that unlike with the
tor-gencert program, the entire file contents are read and used as
@@ -2472,6 +2482,23 @@ is non-zero):
Note: do not use MyFamily when configuring your Tor instance as a
bridge.
+[[FamilyId]] **FamilyId** __ident__::
+ Configure this relay to be part of a family
+ identified by a shared secret family key with the given key identity.
+ A corresponding family key must be stored in the relay's key directory.
+ This option can appear multiple times.
+ Family keys are generated with "--keygen-family";
+ this also generates the value you should use in the __ident__ field
+ in a file ending with ".public\_family\_id".
+ For information on generating and installing a family
+ key, see https://community.torproject.org/relay/setup/post-install/family-ids/
+ +
+ In the future, this will be the preferred way for relays
+ to advertise family membership.
+ But for now, relay families should configure
+ both this option _and_ MyFamily, so older clients
+ will still recognize the relays' family membership.
+
[[Nickname]] **Nickname** __name__::
Set the server's nickname to \'name'. Nicknames must be between 1 and 19
characters inclusive, and must contain only the characters [a-zA-Z0-9].
@@ -4040,6 +4067,12 @@ __KeyDirectory__/**`secret_onion_key_ntor`** and **`secret_onion_key_ntor.old`**
generated key, which the relay uses to handle any requests that were made
by clients that didn't have the new one.
+__KeyDirectory__/__keyname__**`.secret_family_key`**::
+ A relay family's family identity key.
+ Used to prove membership in a relay family.
+ See https://community.torproject.org/relay/setup/post-install/family-ids/
+ for more information.
+
__DataDirectory__/**`fingerprint`**::
Only used by servers. Contains the fingerprint of the server's RSA
identity key.
diff --git a/src/app/config/config.c b/src/app/config/config.c
@@ -470,6 +470,7 @@ static const config_var_t option_vars_[] = {
V(UseDefaultFallbackDirs, BOOL, "1"),
OBSOLETE("FallbackNetworkstatusFile"),
+ VAR("FamilyId", LINELIST, FamilyId_lines, NULL),
V(FascistFirewall, BOOL, "0"),
V(FirewallPorts, CSV, ""),
OBSOLETE("FastFirstHopPK"),
@@ -1049,6 +1050,11 @@ options_clear_cb(const config_mgr_t *mgr, void *opts)
tor_free(options->command_arg);
tor_free(options->master_key_fname);
config_free_lines(options->MyFamily);
+ if (options->FamilyIds) {
+ SMARTLIST_FOREACH(options->FamilyIds,
+ ed25519_public_key_t *, k, tor_free(k));
+ smartlist_free(options->FamilyIds);
+ }
}
/** Release all memory allocated in options
@@ -2488,6 +2494,9 @@ static const struct {
.command=CMD_LIST_FINGERPRINT },
{ .name="--keygen",
.command=CMD_KEYGEN },
+ { .name="--keygen-family",
+ .command=CMD_KEYGEN_FAMILY,
+ .takes_argument=ARGUMENT_NECESSARY },
{ .name="--key-expiration",
.takes_argument=ARGUMENT_OPTIONAL,
.command=CMD_KEY_EXPIRATION },
diff --git a/src/app/config/or_options_st.h b/src/app/config/or_options_st.h
@@ -493,6 +493,10 @@ struct or_options_t {
struct config_line_t *MyFamily_lines; /**< Declared family for this OR. */
struct config_line_t *MyFamily; /**< Declared family for this OR,
normalized */
+ struct config_line_t *FamilyId_lines; /**< If set, IDs for family keys to use
+ * to certify this OR's membership. */
+ struct smartlist_t *FamilyIds; /**< FamilyIds, parsed and converted
+ * to a list of ed25519_public_key_t */
struct config_line_t *NodeFamilies; /**< List of config lines for
* node families */
/** List of parsed NodeFamilies values. */
diff --git a/src/app/config/tor_cmdline_mode.h b/src/app/config/tor_cmdline_mode.h
@@ -23,6 +23,7 @@ typedef enum {
CMD_VERIFY_CONFIG, /**< Running --verify-config. */
CMD_DUMP_CONFIG, /**< Running --dump-config. */
CMD_KEYGEN, /**< Running --keygen */
+ CMD_KEYGEN_FAMILY, /**< Running --keygen-family */
CMD_KEY_EXPIRATION, /**< Running --key-expiration */
CMD_IMMEDIATE, /**< Special value: indicates a command that is handled
* immediately during configuration processing. */
diff --git a/src/app/main/main.c b/src/app/main/main.c
@@ -187,6 +187,11 @@ do_hup(void)
generate_ed_link_cert(options, now, new_signing_key > 0)) {
log_warn(LD_OR, "Problem reloading Ed25519 keys; still using old keys.");
}
+ if (load_family_id_keys(options,
+ networkstatus_get_latest_consensus())) {
+ log_warn(LD_OR, "Problem reloading family ID keys; "
+ "still using old keys.");
+ }
/* Update cpuworker and dnsworker processes, so they get up-to-date
* configuration options. */
@@ -825,6 +830,39 @@ do_dump_config(void)
return 0;
}
+/** Implement --keygen-family; create a family ID key and write it to a file.
+ */
+static int
+do_keygen_family(const char *fname_base)
+{
+ ed25519_public_key_t pk;
+ char *fname_key = NULL, *fname_id = NULL, *id_contents = NULL;
+ int r = -1;
+
+ if (BUG(!fname_base))
+ goto done;
+
+ tor_asprintf(&fname_key, "%s.secret_family_key", fname_base);
+ tor_asprintf(&fname_id, "%s.public_family_id", fname_base);
+
+ if (create_family_id_key(fname_key, &pk) < 0)
+ goto done;
+ tor_asprintf(&id_contents, "%s\n", ed25519_fmt(&pk));
+ if (write_str_to_file(fname_id, id_contents, 0) < 0)
+ goto done;
+
+ printf("# Generated %s\n", fname_key);
+ printf("FamilyId %s\n", ed25519_fmt(&pk));
+
+ r = 0;
+
+ done:
+ tor_free(fname_key);
+ tor_free(fname_id);
+ tor_free(id_contents);
+ return r;
+}
+
static void
init_addrinfo(void)
{
@@ -1382,6 +1420,9 @@ tor_run_main(const tor_main_configuration_t *tor_cfg)
case CMD_KEYGEN:
result = load_ed_keys(get_options(), time(NULL)) < 0;
break;
+ case CMD_KEYGEN_FAMILY:
+ result = do_keygen_family(get_options()->command_arg);
+ break;
case CMD_KEY_EXPIRATION:
init_keys();
result = log_cert_expiration();
diff --git a/src/app/main/ntmain.c b/src/app/main/ntmain.c
@@ -341,6 +341,7 @@ nt_service_main(void)
case CMD_VERIFY_CONFIG:
case CMD_DUMP_CONFIG:
case CMD_KEYGEN:
+ case CMD_KEYGEN_FAMILY:
case CMD_KEY_EXPIRATION:
log_err(LD_CONFIG, "Unsupported command (--list-fingerprint, "
"--hash-password, --keygen, --dump-config, --verify-config, "
diff --git a/src/core/or/protover.c b/src/core/or/protover.c
@@ -386,10 +386,10 @@ protocol_list_supports_protocol_or_later(const char *list,
/*
* XXX START OF HAZARDOUS ZONE XXX
*/
-/* All protocol version that this relay version supports. */
+/* All protocol version that this version of tor supports. */
#define PR_CONFLUX_V "1"
#define PR_CONS_V "1-2"
-#define PR_DESC_V "1-3"
+#define PR_DESC_V "1-4"
#define PR_DIRCACHE_V "2"
#define PR_FLOWCTRL_V "1-2"
#define PR_HSDIR_V "2"
diff --git a/src/feature/dirauth/dirvote.c b/src/feature/dirauth/dirvote.c
@@ -3900,6 +3900,13 @@ dirvote_create_microdescriptor(const routerinfo_t *ri, int consensus_method)
tor_free(canonical_family);
}
+ if (consensus_method >= MIN_METHOD_FOR_FAMILY_IDS &&
+ ri->family_ids && smartlist_len(ri->family_ids)) {
+ char *family_ids = smartlist_join_strings(ri->family_ids, " ", 0, NULL);
+ smartlist_add_asprintf(chunks, "family-ids %s\n", family_ids);
+ tor_free(family_ids);
+ }
+
if (summary && strcmp(summary, "reject 1-65535"))
smartlist_add_asprintf(chunks, "p %s\n", summary);
@@ -3995,6 +4002,8 @@ static const struct consensus_method_range_t {
int high;
} microdesc_consensus_methods[] = {
{MIN_SUPPORTED_CONSENSUS_METHOD,
+ MIN_METHOD_FOR_FAMILY_IDS - 1},
+ {MIN_METHOD_FOR_FAMILY_IDS,
MAX_SUPPORTED_CONSENSUS_METHOD},
{-1, -1}
};
diff --git a/src/feature/dirauth/dirvote.h b/src/feature/dirauth/dirvote.h
@@ -53,7 +53,7 @@
#define MIN_SUPPORTED_CONSENSUS_METHOD 32
/** The highest consensus method that we currently support. */
-#define MAX_SUPPORTED_CONSENSUS_METHOD 34
+#define MAX_SUPPORTED_CONSENSUS_METHOD 35
/**
* Lowest consensus method for which we suppress the published time in
@@ -67,6 +67,12 @@
**/
#define MIN_METHOD_TO_OMIT_PACKAGE_FINGERPRINTS 34
+/**
+ * Lowest supported consensus method for which we include `family-ids`
+ * in microdescs.
+ */
+#define MIN_METHOD_FOR_FAMILY_IDS 35
+
/** Default bandwidth to clip unmeasured bandwidths to using method >=
* MIN_METHOD_TO_CLIP_UNMEASURED_BW. (This is not a consensus method; do not
* get confused with the above macros.) */
diff --git a/src/feature/dirparse/microdesc_parse.c b/src/feature/dirparse/microdesc_parse.c
@@ -35,6 +35,7 @@ static token_rule_t microdesc_token_table[] = {
T0N("id", K_ID, GE(2), NO_OBJ ),
T0N("a", K_A, GE(1), NO_OBJ ),
T01("family", K_FAMILY, CONCAT_ARGS, NO_OBJ ),
+ T01("family-ids", K_FAMILY_IDS, CONCAT_ARGS, NO_OBJ ),
T01("p", K_P, CONCAT_ARGS, NO_OBJ ),
T01("p6", K_P6, CONCAT_ARGS, NO_OBJ ),
A01("@last-listed", A_LAST_LISTED, CONCAT_ARGS, NO_OBJ ),
@@ -252,6 +253,16 @@ microdesc_parse_fields(microdesc_t *md,
NULL,
NF_WARN_MALFORMED);
}
+ if ((tok = find_opt_by_keyword(tokens, K_FAMILY_IDS))) {
+ smartlist_t *ids = smartlist_new();
+ smartlist_split_string(ids, tok->args[0], " ",
+ SPLIT_SKIP_SPACE|SPLIT_IGNORE_BLANK, 0);
+ if (smartlist_len(ids) > 0) {
+ md->family_ids = ids;
+ } else {
+ smartlist_free(ids);
+ }
+ }
if ((tok = find_opt_by_keyword(tokens, K_P))) {
md->exit_policy = parse_short_policy(tok->args[0]);
diff --git a/src/feature/dirparse/parsecommon.h b/src/feature/dirparse/parsecommon.h
@@ -45,6 +45,8 @@ typedef enum {
K_UPTIME,
K_DIR_SIGNING_KEY,
K_FAMILY,
+ K_FAMILY_CERT,
+ K_FAMILY_IDS,
K_FINGERPRINT,
K_HIBERNATING,
K_READ_HISTORY,
diff --git a/src/feature/dirparse/routerparse.c b/src/feature/dirparse/routerparse.c
@@ -51,6 +51,7 @@
**/
#define ROUTERDESC_TOKEN_TABLE_PRIVATE
+#define ROUTERPARSE_PRIVATE
#include "core/or/or.h"
#include "app/config/config.h"
@@ -114,6 +115,7 @@ const token_rule_t routerdesc_token_table[] = {
T01("allow-single-hop-exits",K_ALLOW_SINGLE_HOP_EXITS, NO_ARGS, NO_OBJ ),
T01("family", K_FAMILY, ARGS, NO_OBJ ),
+ T0N("family-cert", K_FAMILY_CERT, ARGS, NEED_OBJ ),
T01("caches-extra-info", K_CACHES_EXTRA_INFO, NO_ARGS, NO_OBJ ),
T0N("or-address", K_OR_ADDRESS, GE(1), NO_OBJ ),
@@ -172,6 +174,10 @@ static token_rule_t extrainfo_token_table[] = {
/* static function prototypes */
static int router_add_exit_policy(routerinfo_t *router,directory_token_t *tok);
static smartlist_t *find_all_exitpolicy(smartlist_t *s);
+static int check_family_certs(const smartlist_t *family_cert_tokens,
+ const ed25519_public_key_t *identity_key,
+ smartlist_t **family_ids_out,
+ time_t *family_expiration_out);
/** Set <b>digest</b> to the SHA-1 digest of the hash of the first router in
* <b>s</b>. Return 0 on success, -1 on failure.
@@ -894,6 +900,21 @@ router_parse_entry_from_string(const char *s, const char *end,
}
}
+ {
+ smartlist_t *family_cert_toks = find_all_by_keyword(tokens, K_FAMILY_CERT);
+ time_t family_expiration = TIME_MAX;
+ int r = 0;
+ if (family_cert_toks) {
+ r = check_family_certs(family_cert_toks,
+ &router->cache_info.signing_key_cert->signing_key,
+ &router->family_ids,
+ &family_expiration);
+ smartlist_free(family_cert_toks);
+ }
+ if (r<0)
+ goto err;
+ }
+
if (find_opt_by_keyword(tokens, K_CACHES_EXTRA_INFO))
router->caches_extra_info = 1;
@@ -1250,6 +1271,115 @@ find_all_exitpolicy(smartlist_t *s)
return out;
}
+/**
+ * Parse and validate a single `FAMILY_CERT` token's object.
+ *
+ * Arguments are as for `check_family_certs()`.
+ */
+STATIC int
+check_one_family_cert(const uint8_t *cert_body,
+ size_t cert_body_size,
+ const ed25519_public_key_t *identity_key,
+ char **family_id_out,
+ time_t *family_expiration_out)
+{
+ tor_cert_t *cert = NULL;
+ int r = -1;
+
+ cert = tor_cert_parse(cert_body, cert_body_size);
+
+ if (! cert)
+ goto done;
+ if (cert->cert_type != CERT_TYPE_FAMILY_V_IDENTITY) {
+ log_warn(LD_DIR, "Wrong cert type in family certificate.");
+ goto done;
+ }
+ if (! cert->signing_key_included) {
+ log_warn(LD_DIR, "Missing family key in family certificate.");
+ goto done;
+ }
+ if (! ed25519_pubkey_eq(&cert->signed_key, identity_key)) {
+ log_warn(LD_DIR, "Key mismatch in family certificate.");
+ goto done;
+ }
+
+ time_t valid_until = cert->valid_until;
+
+ /* We're using NULL for the key, since the cert has the signing key included.
+ * We're using 0 for "now", since we're going to extract the expiration
+ * separately.
+ */
+ if (tor_cert_checksig(cert, NULL, 0) < 0) {
+ log_warn(LD_DIR, "Invalid signature in family certificate");
+ goto done;
+ }
+
+ /* At this point we know that the cert is valid.
+ * We extract the expiration time and the signing key. */
+ *family_expiration_out = valid_until;
+
+ char buf[ED25519_BASE64_LEN+1];
+ ed25519_public_to_base64(buf, &cert->signing_key);
+ tor_asprintf(family_id_out, "ed25519:%s", buf);
+
+ r = 0;
+ done:
+ tor_cert_free(cert);
+ return r;
+}
+
+/**
+ * Given a list of `FAMILY_CERT` tokens, and a relay's ed25519 `identity_key`,
+ * validate the family certificates in all the tokens, and convert them into
+ * family IDs in a newly allocated `family_ids_out` list.
+ * Set `family_expiration_out` to the earliest time at which any certificate
+ * in the list expires.
+ * Return 0 on success, and -1 on failure.
+ */
+static int
+check_family_certs(const smartlist_t *family_cert_tokens,
+ const ed25519_public_key_t *identity_key,
+ smartlist_t **family_ids_out,
+ time_t *family_expiration_out)
+{
+ if (BUG(!identity_key) ||
+ BUG(!family_ids_out) ||
+ BUG(!family_expiration_out))
+ return -1;
+
+ *family_expiration_out = TIME_MAX;
+
+ if (family_cert_tokens == NULL || smartlist_len(family_cert_tokens) == 0) {
+ *family_ids_out = NULL;
+ return 0;
+ }
+
+ *family_ids_out = smartlist_new();
+ SMARTLIST_FOREACH_BEGIN(family_cert_tokens, directory_token_t *, tok) {
+ if (BUG(tok->object_body == NULL))
+ goto err;
+
+ char *this_id = NULL;
+ time_t this_expiration = TIME_MAX;
+ if (check_one_family_cert((const uint8_t*)tok->object_body,
+ tok->object_size,
+ identity_key,
+ &this_id, &this_expiration) < 0)
+ goto err;
+ smartlist_add(*family_ids_out, this_id);
+ *family_expiration_out = MIN(*family_expiration_out, this_expiration);
+ } SMARTLIST_FOREACH_END(tok);
+
+ smartlist_sort_strings(*family_ids_out);
+ smartlist_uniq_strings(*family_ids_out);
+
+ return 0;
+ err:
+ SMARTLIST_FOREACH(*family_ids_out, char *, cp, tor_free(cp));
+ smartlist_free(*family_ids_out);
+ return -1;
+}
+
/** Called on startup; right now we just handle scanning the unparseable
* descriptor dumps, but hang anything else we might need to do in the
* future here as well.
diff --git a/src/feature/dirparse/routerparse.h b/src/feature/dirparse/routerparse.h
@@ -12,6 +12,8 @@
#ifndef TOR_ROUTERPARSE_H
#define TOR_ROUTERPARSE_H
+#include "lib/testsupport/testsupport.h"
+
int router_get_router_hash(const char *s, size_t s_len, char *digest);
int router_get_extrainfo_hash(const char *s, size_t s_len, char *digest);
@@ -47,4 +49,12 @@ extern const struct token_rule_t routerdesc_token_table[];
#define ED_DESC_SIGNATURE_PREFIX "Tor router descriptor signature v1"
+#ifdef ROUTERPARSE_PRIVATE
+STATIC int check_one_family_cert(const uint8_t *cert_body,
+ size_t cert_body_size,
+ const struct ed25519_public_key_t *identity_key,
+ char **family_id_out,
+ time_t *family_expiration_out);
+#endif
+
#endif /* !defined(TOR_ROUTERPARSE_H) */
diff --git a/src/feature/nodelist/microdesc.c b/src/feature/nodelist/microdesc.c
@@ -915,6 +915,10 @@ microdesc_free_(microdesc_t *md, const char *fname, int lineno)
tor_free(md->body);
nodefamily_free(md->family);
+ if (md->family_ids) {
+ SMARTLIST_FOREACH(md->family_ids, char *, cp, tor_free(cp));
+ smartlist_free(md->family_ids);
+ }
short_policy_free(md->exit_policy);
short_policy_free(md->ipv6_exit_policy);
diff --git a/src/feature/nodelist/microdesc_st.h b/src/feature/nodelist/microdesc_st.h
@@ -16,6 +16,7 @@ struct curve25519_public_key_t;
struct ed25519_public_key_t;
struct nodefamily_t;
struct short_policy_t;
+struct smartlist_t;
#include "ext/ht.h"
@@ -73,6 +74,11 @@ struct microdesc_t {
uint16_t ipv6_orport;
/** As routerinfo_t.family, with readable members parsed. */
struct nodefamily_t *family;
+ /** A list of strings representing router family IDs.
+ * May be null; Copied from family-ids.
+ * (Happy families only.) */
+ struct smartlist_t *family_ids;
+
/** IPv4 exit policy summary */
struct short_policy_t *exit_policy;
/** IPv6 exit policy summary */
diff --git a/src/feature/nodelist/nodelist.c b/src/feature/nodelist/nodelist.c
@@ -680,6 +680,32 @@ get_estimated_address_per_node, (void))
return ESTIMATED_ADDRESS_PER_NODE;
}
+/**
+ * If true, we use relays' listed family members in order to
+ * determine which relays are in the same family.
+ */
+static int use_family_lists = 1;
+/**
+ * If true, we use relays' validated family IDs in order to
+ * determine which relays are in the same family.
+ */
+static int use_family_ids = 1;
+
+/**
+ * Update consensus parameters relevant to nodelist operations.
+ *
+ * We need to cache these values rather than searching for them every time
+ * we check whether two relays are in the same family.
+ **/
+static void
+nodelist_update_consensus_params(const networkstatus_t *ns)
+{
+ use_family_lists = networkstatus_get_param(ns, "use-family-lists",
+ 1, 0, 1); // default, low, high
+ use_family_ids = networkstatus_get_param(ns, "use-family-ids",
+ 1, 0, 1); // default, low, high
+}
+
/** Tell the nodelist that the current usable consensus is <b>ns</b>.
* This makes the nodelist change all of the routerstatus entries for
* the nodes, drop nodes that no longer have enough info to get used,
@@ -698,6 +724,8 @@ nodelist_set_consensus(const networkstatus_t *ns)
SMARTLIST_FOREACH(the_nodelist->nodes, node_t *, node,
node->rs = NULL);
+ nodelist_update_consensus_params(ns);
+
/* Conservatively estimate that every node will have 2 addresses (v4 and
* v6). Then we add the number of configured trusted authorities we have. */
int estimated_addresses = smartlist_len(ns->routerstatus_list) *
@@ -2114,7 +2142,7 @@ node_in_nickname_smartlist(const smartlist_t *lst, const node_t *node)
/** Return true iff n1's declared family contains n2. */
STATIC int
-node_family_contains(const node_t *n1, const node_t *n2)
+node_family_list_contains(const node_t *n1, const node_t *n2)
{
if (n1->ri && n1->ri->declared_family) {
return node_in_nickname_smartlist(n1->ri->declared_family, n2);
@@ -2129,7 +2157,7 @@ node_family_contains(const node_t *n1, const node_t *n2)
* Return true iff <b>node</b> has declared a nonempty family.
**/
STATIC bool
-node_has_declared_family(const node_t *node)
+node_has_declared_family_list(const node_t *node)
{
if (node->ri && node->ri->declared_family &&
smartlist_len(node->ri->declared_family)) {
@@ -2144,12 +2172,44 @@ node_has_declared_family(const node_t *node)
}
/**
+ * Return the listed family IDs of `a`, if it has any.
+ */
+static const smartlist_t *
+node_get_family_ids(const node_t *node)
+{
+ if (node->ri && node->ri->family_ids) {
+ return node->ri->family_ids;
+ } else if (node->md && node->md->family_ids) {
+ return node->md->family_ids;
+ } else {
+ return NULL;
+ }
+}
+
+/**
+ * Return true iff `a` and `b` have any family ID in common.
+ **/
+static bool
+nodes_have_common_family_id(const node_t *a, const node_t *b)
+{
+ const smartlist_t *ids_a = node_get_family_ids(a);
+ const smartlist_t *ids_b = node_get_family_ids(b);
+ if (ids_a == NULL || ids_b == NULL)
+ return false;
+ SMARTLIST_FOREACH(ids_a, const char *, id, {
+ if (smartlist_contains_string(ids_b, id))
+ return true;
+ });
+ return false;
+}
+
+/**
* Add to <b>out</b> every node_t that is listed by <b>node</b> as being in
* its family. (Note that these nodes are not in node's family unless they
* also agree that node is in their family.)
**/
STATIC void
-node_lookup_declared_family(smartlist_t *out, const node_t *node)
+node_lookup_declared_family_list(smartlist_t *out, const node_t *node)
{
if (node->ri && node->ri->declared_family &&
smartlist_len(node->ri->declared_family)) {
@@ -2189,9 +2249,17 @@ nodes_in_same_family(const node_t *node1, const node_t *node2)
return 1;
}
- /* Are they in the same family because the agree they are? */
- if (node_family_contains(node1, node2) &&
- node_family_contains(node2, node1)) {
+ /* Are they in the same family because they agree they are? */
+ if (use_family_lists &&
+ node_family_list_contains(node1, node2) &&
+ node_family_list_contains(node2, node1)) {
+ return 1;
+ }
+
+ /* Are they in the same family because they have a common
+ * verified family ID? */
+ if (use_family_ids &&
+ nodes_have_common_family_id(node1, node2)) {
return 1;
}
@@ -2251,20 +2319,31 @@ nodelist_add_node_and_family(smartlist_t *sl, const node_t *node)
/* Now, add all nodes in the declared family of this node, if they
* also declare this node to be in their family. */
- if (node_has_declared_family(node)) {
+ if (use_family_lists &&
+ node_has_declared_family_list(node)) {
smartlist_t *declared_family = smartlist_new();
- node_lookup_declared_family(declared_family, node);
+ node_lookup_declared_family_list(declared_family, node);
/* Add every r such that router declares familyness with node, and node
* declares familyhood with router. */
SMARTLIST_FOREACH_BEGIN(declared_family, const node_t *, node2) {
- if (node_family_contains(node2, node)) {
+ if (node_family_list_contains(node2, node)) {
smartlist_add(sl, (void*)node2);
}
} SMARTLIST_FOREACH_END(node2);
smartlist_free(declared_family);
}
+ /* Now add all the nodes that share a verified family ID with this node. */
+ if (use_family_ids &&
+ node_get_family_ids(node)) {
+ SMARTLIST_FOREACH(all_nodes, const node_t *, node2, {
+ if (nodes_have_common_family_id(node, node2)) {
+ smartlist_add(sl, (void *)node2);
+ }
+ });
+ }
+
/* If the user declared any families locally, honor those too. */
if (options->NodeFamilySets) {
SMARTLIST_FOREACH(options->NodeFamilySets, const routerset_t *, rs, {
diff --git a/src/feature/nodelist/nodelist.h b/src/feature/nodelist/nodelist.h
@@ -170,9 +170,10 @@ int count_loading_descriptors_progress(void);
STATIC int node_nickname_matches(const node_t *node, const char *nickname);
STATIC int node_in_nickname_smartlist(const smartlist_t *lst,
const node_t *node);
-STATIC int node_family_contains(const node_t *n1, const node_t *n2);
-STATIC bool node_has_declared_family(const node_t *node);
-STATIC void node_lookup_declared_family(smartlist_t *out, const node_t *node);
+STATIC int node_family_list_contains(const node_t *n1, const node_t *n2);
+STATIC bool node_has_declared_family_list(const node_t *node);
+STATIC void node_lookup_declared_family_list(smartlist_t *out,
+ const node_t *node);
#ifdef TOR_UNIT_TESTS
diff --git a/src/feature/nodelist/routerinfo_st.h b/src/feature/nodelist/routerinfo_st.h
@@ -15,6 +15,7 @@
#include "feature/nodelist/signed_descriptor_st.h"
struct curve25519_public_key_t;
+struct smartlist_t;
/** Information about another onion router in the network. */
struct routerinfo_t {
@@ -67,6 +68,10 @@ struct routerinfo_t {
long uptime; /**< How many seconds the router claims to have been up */
smartlist_t *declared_family; /**< Nicknames of router which this router
* claims are its family. */
+ /** A list of strings representing router family IDs.
+ * May be null. Extracted from family-certs.
+ * (Happy families only.) */
+ struct smartlist_t *family_ids;
char *contact_info; /**< Declared contact info for this router. */
unsigned int is_hibernating:1; /**< Whether the router claims to be
* hibernating */
diff --git a/src/feature/nodelist/routerlist.c b/src/feature/nodelist/routerlist.c
@@ -940,6 +940,10 @@ routerinfo_free_(routerinfo_t *router)
SMARTLIST_FOREACH(router->declared_family, char *, s, tor_free(s));
smartlist_free(router->declared_family);
}
+ if (router->family_ids) {
+ SMARTLIST_FOREACH(router->family_ids, char *, cp, tor_free(cp));
+ smartlist_free(router->family_ids);
+ }
addr_policy_list_free(router->exit_policy);
short_policy_free(router->ipv6_exit_policy);
diff --git a/src/feature/nodelist/torcert.h b/src/feature/nodelist/torcert.h
@@ -22,6 +22,7 @@
#define CERT_TYPE_AUTH_HS_IP_KEY 0x09
#define CERT_TYPE_ONION_ID 0x0A
#define CERT_TYPE_CROSS_HS_IP_KEYS 0x0B
+#define CERT_TYPE_FAMILY_V_IDENTITY 0x0C
#define CERT_FLAG_INCLUDE_SIGNING_KEY 0x1
diff --git a/src/feature/relay/relay_config.c b/src/feature/relay/relay_config.c
@@ -21,6 +21,7 @@
#include "lib/meminfo/meminfo.h"
#include "lib/osinfo/uname.h"
#include "lib/process/setuid.h"
+#include "lib/crypt_ops/crypto_format.h"
/* Required for dirinfo_type_t in or_options_t */
#include "core/or/or.h"
@@ -1180,6 +1181,19 @@ options_validate_relay_mode(const or_options_t *old_options,
options->MyFamily_lines, "MyFamily", msg))
return -1;
+ if (options->FamilyId_lines) {
+ options->FamilyIds = smartlist_new();
+ config_line_t *line;
+ for (line = options->FamilyId_lines; line; line = line->next) {
+ ed25519_public_key_t pk;
+ if (ed25519_public_from_base64(&pk, line->value) < 0) {
+ tor_asprintf(msg, "Invalid FamilyId %s", line->value);
+ return -1;
+ }
+ smartlist_add(options->FamilyIds, tor_memdup(&pk, sizeof(pk)));
+ }
+ }
+
if (options->ConstrainedSockets) {
if (options->DirPort_set) {
/* Providing cached directory entries while system TCP buffers are scarce
@@ -1274,6 +1288,7 @@ options_transition_affects_descriptor(const or_options_t *old_options,
YES_IF_CHANGED_STRING(ContactInfo);
YES_IF_CHANGED_STRING(BridgeDistribution);
YES_IF_CHANGED_LINELIST(MyFamily);
+ YES_IF_CHANGED_LINELIST(FamilyId_lines);
YES_IF_CHANGED_STRING(AccountingStart);
YES_IF_CHANGED_INT(AccountingMax);
YES_IF_CHANGED_INT(AccountingRule);
diff --git a/src/feature/relay/router.c b/src/feature/relay/router.c
@@ -876,6 +876,7 @@ router_initialize_tls_context(void)
STATIC void
router_announce_bridge_status_page(void)
{
+#ifdef ENABLE_MODULE_RELAY
char fingerprint[FINGERPRINT_LEN + 1];
if (crypto_pk_get_hashed_fingerprint(get_server_identity_key(),
@@ -889,6 +890,7 @@ router_announce_bridge_status_page(void)
log_notice(LD_GENERAL, "You can check the status of your bridge relay at "
"https://bridges.torproject.org/status?id=%s",
fingerprint);
+#endif
}
/** Compute fingerprint (or hashed fingerprint if hashed is 1) and write
@@ -1065,6 +1067,11 @@ init_keys(void)
if (new_signing_key < 0)
return -1;
+ if (options->command == CMD_RUN_TOR) {
+ if (load_family_id_keys(options, networkstatus_get_latest_consensus()) < 0)
+ return -1;
+ }
+
/* 2. Read onion key. Make it if none is found. */
keydir = get_keydir_fname("secret_onion_key");
log_info(LD_GENERAL,"Reading/making onion key \"%s\"...",keydir);
@@ -2528,6 +2535,21 @@ router_new_consensus_params(const networkstatus_t *ns)
publish_even_when_ipv4_orport_unreachable = ar;
publish_even_when_ipv6_orport_unreachable = ar || ar6;
+
+ warn_about_family_id_config(get_options(), ns);
+}
+
+/**
+ * Return true if the parameters in `ns` say that we should publish
+ * a legacy family list.
+ *
+ * Use the latest networkstatus (or returns the default) if `ns` is NULL.
+ */
+bool
+should_publish_family_list(const networkstatus_t *ns)
+{
+ return networkstatus_get_param(ns, "publish-family-list",
+ 1, 0, 1); // default, min, max
}
/** Mark our descriptor out of data iff the IPv6 omit status flag is flipped
@@ -3034,6 +3056,35 @@ router_dump_router_to_string(routerinfo_t *router,
we_are_hibernating() ? "hibernating 1\n" : "",
"hidden-service-dir\n");
+ SMARTLIST_FOREACH_BEGIN(get_current_family_id_keys(),
+ const ed25519_keypair_t *, k_family_id) {
+ // TODO PROP321: We may want this to be configurable;
+ // we can probably use a smaller value.
+#define FAMILY_CERT_LIFETIME (30*86400)
+ tor_cert_t *family_cert = tor_cert_create_ed25519(
+ k_family_id,
+ CERT_TYPE_FAMILY_V_IDENTITY,
+ // (this is the identity key "KP_relayid_ed")
+ &router->cache_info.signing_key_cert->signing_key,
+ router->cache_info.published_on,
+ FAMILY_CERT_LIFETIME, CERT_FLAG_INCLUDE_SIGNING_KEY);
+ char family_cert_base64[256];
+ if (base64_encode(family_cert_base64, sizeof(family_cert_base64),
+ (const char*) family_cert->encoded,
+ family_cert->encoded_len, BASE64_ENCODE_MULTILINE) < 0) {
+ log_err(LD_BUG, "Base64 encoding family cert failed!?");
+ tor_cert_free(family_cert);
+ goto err;
+ }
+ smartlist_add_asprintf(chunks,
+ "family-cert\n"
+ "-----BEGIN FAMILY CERT-----\n"
+ "%s"
+ "-----END FAMILY CERT-----\n",
+ family_cert_base64);
+ tor_cert_free(family_cert);
+ } SMARTLIST_FOREACH_END(k_family_id);
+
if (options->ContactInfo && strlen(options->ContactInfo)) {
const char *ci = options->ContactInfo;
if (strchr(ci, '\n') || strchr(ci, '\r'))
diff --git a/src/feature/relay/router.h b/src/feature/relay/router.h
@@ -81,6 +81,8 @@ void consider_publishable_server(int force);
int should_refuse_unknown_exits(const or_options_t *options);
void router_new_consensus_params(const networkstatus_t *);
+bool should_publish_family_list(const networkstatus_t *ns);
+
void router_upload_dir_desc_to_dirservers(int force);
void mark_my_descriptor_dirty_if_too_old(time_t now);
void mark_my_descriptor_dirty(const char *reason);
diff --git a/src/feature/relay/routerkeys.c b/src/feature/relay/routerkeys.c
@@ -14,6 +14,8 @@
* (TODO: The keys in router.c should go here too.)
*/
+#define ROUTERKEYS_PRIVATE
+
#include "core/or/or.h"
#include "app/config/config.h"
#include "feature/relay/router.h"
@@ -21,8 +23,11 @@
#include "feature/relay/routermode.h"
#include "feature/keymgt/loadkey.h"
#include "feature/nodelist/torcert.h"
+#include "feature/nodelist/networkstatus_st.h"
+#include "feature/dirauth/dirvote.h"
#include "lib/crypt_ops/crypto_util.h"
+#include "lib/crypt_ops/crypto_format.h"
#include "lib/tls/tortls.h"
#include "lib/tls/x509.h"
@@ -44,6 +49,9 @@ static uint8_t *rsa_ed_crosscert = NULL;
static size_t rsa_ed_crosscert_len = 0;
static time_t rsa_ed_crosscert_expiration = 0;
+// list of ed25519_keypair_t
+static smartlist_t *family_id_keys = NULL;
+
/**
* Running as a server: load, reload, or refresh our ed25519 keys and
* certificates, creating and saving new ones as needed.
@@ -674,6 +682,275 @@ get_current_auth_key_cert(void)
return auth_key_cert;
}
+/**
+ * Suffix for the filenames in which we expect to find a family ID key.
+ */
+#define FAMILY_KEY_SUFFIX ".secret_family_key"
+
+/**
+ * Return true if `fname` is a possible filename of a family ID key.
+ *
+ * Family ID key filenames are FAMILY_KEY_FNAME, followed optionally
+ * by "." and a positive integer.
+ */
+STATIC bool
+is_family_key_fname(const char *fname)
+{
+ return 0 == strcmpend(fname, FAMILY_KEY_SUFFIX);
+}
+
+/** Return true if `id` is configured in `options`. */
+static bool
+family_key_id_is_expected(const or_options_t *options,
+ const ed25519_public_key_t *id)
+{
+ SMARTLIST_FOREACH(options->FamilyIds, const ed25519_public_key_t *, k, {
+ if (ed25519_pubkey_eq(k, id))
+ return true;
+ });
+ return false;
+}
+
+/** Return true if the key for `id` has been loaded. */
+static bool
+family_key_is_present(const ed25519_public_key_t *id)
+{
+ if (!family_id_keys)
+ return false;
+
+ SMARTLIST_FOREACH(family_id_keys, const ed25519_keypair_t *, kp, {
+ if (ed25519_pubkey_eq(&kp->pubkey, id))
+ return true;
+ });
+ return false;
+}
+
+/**
+ * Tag to use on family key files.
+ */
+#define FAMILY_KEY_FILE_TAG "fmly-id"
+
+/**
+ * Look for all the family keys in `keydir`, load them into
+ * family_id_keys.
+ */
+STATIC int
+load_family_id_keys_impl(const or_options_t *options,
+ const char *keydir)
+{
+ if (BUG(!options) || BUG(!keydir))
+ return -1;
+
+ smartlist_t *files = tor_listdir(keydir);
+ smartlist_t *new_keys = NULL;
+ ed25519_keypair_t *kp_tmp = NULL;
+ char *fn_tmp = NULL;
+ char *tag_tmp = NULL;
+ int r = -1;
+
+ if (files == NULL) {
+ log_warn(LD_OR, "Unable to list contents of directory %s", keydir);
+ goto end;
+ }
+
+ new_keys = smartlist_new();
+ SMARTLIST_FOREACH_BEGIN(files, const char *, fn) {
+ if (!is_family_key_fname(fn))
+ continue;
+
+ tor_asprintf(&fn_tmp, "%s%s%s", keydir, PATH_SEPARATOR, fn);
+
+ kp_tmp = tor_malloc_zero(sizeof(*kp_tmp));
+ // TODO: If we ever allow cert provisioning here,
+ // use ed_key_init_from_file() instead.
+ if (ed25519_seckey_read_from_file(&kp_tmp->seckey, &tag_tmp, fn_tmp) < 0) {
+ log_warn(LD_OR, "%s was not an ed25519 secret key.", fn_tmp);
+ goto end;
+ }
+ if (0 != strcmp(tag_tmp, FAMILY_KEY_FILE_TAG)) {
+ log_warn(LD_OR, "%s was not a family ID key.", fn_tmp);
+ goto end;
+ }
+ if (ed25519_public_key_generate(&kp_tmp->pubkey, &kp_tmp->seckey) < 0) {
+ log_warn(LD_OR, "Unable to generate public key for %s", fn_tmp);
+ goto end;
+ }
+
+ if (family_key_id_is_expected(options, &kp_tmp->pubkey)) {
+ smartlist_add(new_keys, kp_tmp);
+ kp_tmp = NULL; // prevent double-free
+ } else {
+ log_warn(LD_OR, "Found secret family key in %s "
+ "with unexpected FamilyID %s",
+ fn_tmp, ed25519_fmt(&kp_tmp->pubkey));
+ }
+
+ tor_free(fn_tmp);
+ tor_free(tag_tmp);
+ } SMARTLIST_FOREACH_END(fn);
+
+ set_family_id_keys(new_keys);
+ new_keys = NULL; // prevent double-free
+ r = 0;
+ end:
+ if (files) {
+ SMARTLIST_FOREACH(files, char *, cp, tor_free(cp));
+ smartlist_free(files);
+ }
+ if (new_keys) {
+ SMARTLIST_FOREACH(new_keys, ed25519_keypair_t *, kp,
+ ed25519_keypair_free(kp));
+ smartlist_free(new_keys);
+ }
+ tor_free(fn_tmp);
+ tor_free(tag_tmp);
+ ed25519_keypair_free(kp_tmp);
+ return r;
+}
+
+/**
+ * Create a new family ID key, and store it in `fname`.
+ *
+ * If pk_out is provided, set it to the generated public key.
+ **/
+int
+create_family_id_key(const char *fname, ed25519_public_key_t *pk_out)
+{
+ int r = -1;
+ ed25519_keypair_t *kp = tor_malloc_zero(sizeof(ed25519_keypair_t));
+ if (ed25519_keypair_generate(kp, 1) < 0) {
+ log_warn(LD_BUG, "Can't generate ed25519 key!");
+ goto done;
+ }
+
+ if (ed25519_seckey_write_to_file(&kp->seckey,
+ fname, FAMILY_KEY_FILE_TAG)<0) {
+ log_warn(LD_BUG, "Can't write key to file.");
+ goto done;
+ }
+
+ if (pk_out) {
+ ed25519_pubkey_copy(pk_out, &kp->pubkey);
+ }
+
+ r = 0;
+
+ done:
+ ed25519_keypair_free(kp);
+ return r;
+}
+
+/**
+ * If configured to do so, load our family keys from the key directory.
+ * Otherwise, clear the family keys.
+ *
+ * Additionally, warn about inconsistencies between family options.
+ * If `ns` is provided, provide additional warnings.
+ *
+ * `options` is required; `ns` may be NULL.
+ */
+int
+load_family_id_keys(const or_options_t *options,
+ const networkstatus_t *ns)
+{
+ if (options->FamilyIds) {
+ if (load_family_id_keys_impl(options, options->KeyDirectory) < 0)
+ return -1;
+
+ bool any_missing = false;
+ SMARTLIST_FOREACH_BEGIN(options->FamilyIds,
+ const ed25519_public_key_t *, id) {
+ if (!family_key_is_present(id)) {
+ log_err(LD_OR, "No key was found for listed FamilyID %s",
+ ed25519_fmt(id));
+ any_missing = true;
+ }
+ } SMARTLIST_FOREACH_END(id);
+ if (any_missing)
+ return -1;
+
+ log_info(LD_OR, "Found %d family ID keys",
+ smartlist_len(get_current_family_id_keys()));
+ } else {
+ set_family_id_keys(NULL);
+ }
+ warn_about_family_id_config(options, ns);
+ return 0;
+}
+
+#define FAMILY_INFO_URL \
+ "https://community.torproject.org/relay/setup/post-install/family-ids/"
+
+/** Generate warnings as appropriate about our family ID configuration.
+ *
+ * `options` is required; `ns` may be NULL.
+ */
+void
+warn_about_family_id_config(const or_options_t *options,
+ const networkstatus_t *ns)
+{
+ static int have_warned_absent_myfamily = 0;
+ static int have_warned_absent_familykeys = 0;
+
+ if (options->FamilyIds) {
+ if (!have_warned_absent_myfamily &&
+ !options->MyFamily && ns && should_publish_family_list(ns)) {
+ log_warn(LD_OR,
+ "FamilyId was configured, but MyFamily was not. "
+ "FamilyId is good, but the Tor network still requires "
+ "MyFamily while clients are migrating to use family "
+ "keys instead.");
+ have_warned_absent_myfamily = 1;
+ }
+ } else {
+ if (!have_warned_absent_familykeys &&
+ options->MyFamily &&
+ ns && ns->consensus_method >= MIN_METHOD_FOR_FAMILY_IDS) {
+ log_notice(LD_OR,
+ "MyFamily was configured, but FamilyId was not. "
+ "It's a good time to start migrating your relays "
+ "to use family keys. "
+ "See "FAMILY_INFO_URL " for instructions.");
+ have_warned_absent_familykeys = 1;
+ }
+ }
+}
+
+/**
+ * Return a list of our current family id keypairs,
+ * as a list of `ed25519_keypair_t`.
+ *
+ * Never returns NULL.
+ *
+ * TODO PROP321: Right now this is only used in testing;
+ * when we add relay support we'll need a way to actually
+ * read these keys from disk.
+ **/
+const smartlist_t *
+get_current_family_id_keys(void)
+{
+ if (family_id_keys == NULL)
+ family_id_keys = smartlist_new();
+ return family_id_keys;
+}
+
+/**
+ * Replace our list of family ID keys with `family_id_keys`,
+ * which must be a list of `ed25519_keypair_t`.
+ *
+ * Takes ownership of its input.
+ */
+STATIC void
+set_family_id_keys(smartlist_t *keys)
+{
+ if (family_id_keys) {
+ SMARTLIST_FOREACH(family_id_keys, ed25519_keypair_t *, kp,
+ ed25519_keypair_free(kp));
+ smartlist_free(family_id_keys);
+ }
+ family_id_keys = keys;
+}
+
void
get_master_rsa_crosscert(const uint8_t **cert_out,
size_t *size_out)
@@ -746,6 +1023,8 @@ routerkeys_free_all(void)
ed25519_keypair_free(master_identity_key);
ed25519_keypair_free(master_signing_key);
ed25519_keypair_free(current_auth_key);
+ set_family_id_keys(NULL);
+
tor_cert_free(signing_key_cert);
tor_cert_free(link_cert_cert);
tor_cert_free(auth_key_cert);
diff --git a/src/feature/relay/routerkeys.h b/src/feature/relay/routerkeys.h
@@ -21,6 +21,8 @@ const ed25519_keypair_t *get_current_auth_keypair(void);
const struct tor_cert_st *get_current_link_cert_cert(void);
const struct tor_cert_st *get_current_auth_key_cert(void);
+const smartlist_t *get_current_family_id_keys(void);
+
void get_master_rsa_crosscert(const uint8_t **cert_out,
size_t *size_out);
@@ -39,6 +41,11 @@ uint8_t *make_tap_onion_key_crosscert(const crypto_pk_t *onion_key,
int log_cert_expiration(void);
int load_ed_keys(const or_options_t *options, time_t now);
+int load_family_id_keys(const or_options_t *options,
+ const networkstatus_t *ns);
+int create_family_id_key(const char *fname, ed25519_public_key_t *pk_out);
+void warn_about_family_id_config(const or_options_t *options,
+ const networkstatus_t *ns);
int should_make_new_ed_keys(const or_options_t *options, const time_t now);
int generate_ed_link_cert(const or_options_t *options, time_t now, int force);
@@ -82,6 +89,10 @@ relay_key_is_unavailable_(void)
((void)(options), (void)(now), (void)(force), 0)
#define should_make_new_ed_keys(options, now) \
((void)(options), (void)(now), 0)
+#define warn_about_family_id_config(options,ns) \
+ ((void)(options), (void)(ns))
+#define get_current_family_id_keys() \
+ (smartlist_new())
// These can get removed once router.c becomes relay-only.
static inline struct tor_cert_st *
@@ -120,6 +131,10 @@ make_tap_onion_key_crosscert(const crypto_pk_t *onion_key,
* CMD_KEYGEN. */
#define load_ed_keys(x,y) \
(puts("Not available: Tor has been compiled without relay support"), 0)
+#define load_family_id_keys(x,y) \
+ (puts("Not available: Tor has been compiled without relay support"), 0)
+#define create_family_id_key(x,y) \
+ (puts("Not available: Tor has been compiled without relay support"), -1)
#endif /* defined(HAVE_MODULE_RELAY) */
@@ -128,4 +143,11 @@ const ed25519_keypair_t *get_master_identity_keypair(void);
void init_mock_ed_keys(const crypto_pk_t *rsa_identity_key);
#endif
+#ifdef ROUTERKEYS_PRIVATE
+STATIC void set_family_id_keys(smartlist_t *keys);
+STATIC bool is_family_key_fname(const char *fname);
+STATIC int load_family_id_keys_impl(const or_options_t *options,
+ const char *keydir);
+#endif
+
#endif /* !defined(TOR_ROUTERKEYS_H) */
diff --git a/src/test/test_dir.c b/src/test/test_dir.c
@@ -21,6 +21,8 @@
#define RELAY_PRIVATE
#define ROUTERLIST_PRIVATE
#define ROUTER_PRIVATE
+#define ROUTERKEYS_PRIVATE
+#define ROUTERPARSE_PRIVATE
#define UNPARSEABLE_PRIVATE
#define VOTEFLAGS_PRIVATE
@@ -864,8 +866,21 @@ test_dir_formats_rsa_ed25519(void *arg)
tt_str_op(buf, OP_EQ, buf2);
tor_free(buf);
+ /* We make a couple of changes now before we make the desc that we're going
+ * to parse and check the signature on. */
setup_mock_configured_ports(r2->ipv4_orport, 0);
+ ed25519_keypair_t family_1;
+ ed25519_keypair_t family_2;
+ ed25519_keypair_generate(&family_1, 0);
+ ed25519_keypair_generate(&family_2, 0);
+ {
+ smartlist_t *family_keys = smartlist_new();
+ smartlist_add(family_keys, tor_memdup(&family_1, sizeof(family_1)));
+ smartlist_add(family_keys, tor_memdup(&family_2, sizeof(family_2)));
+ set_family_id_keys(family_keys); // takes ownership.
+ }
+
buf = router_dump_router_to_string(r2, r2->identity_pkey,
r2_onion_pkey,
&r2_onion_keypair, &kp2);
@@ -883,6 +898,20 @@ test_dir_formats_rsa_ed25519(void *arg)
r2->onion_curve25519_pkey->public_key,
CURVE25519_PUBKEY_LEN);
+ // Check family ids.
+ tt_assert(rp2->family_ids != NULL);
+ tt_int_op(smartlist_len(rp2->family_ids), OP_EQ, 2);
+ {
+ char k[ED25519_BASE64_LEN+1];
+ char b[sizeof(k)+16];
+ ed25519_public_to_base64(k, &family_1.pubkey);
+ tor_snprintf(b, sizeof(b), "ed25519:%s", k);
+ tt_assert(smartlist_contains_string(rp2->family_ids, b));
+ ed25519_public_to_base64(k, &family_2.pubkey);
+ tor_snprintf(b, sizeof(b), "ed25519:%s", k);
+ tt_assert(smartlist_contains_string(rp2->family_ids, b));
+ }
+
CHECK_PARSED_EXIT_POLICY(rp2);
tor_free(buf);
@@ -7268,6 +7297,126 @@ test_dir_dirserv_add_own_fingerprint(void *arg)
crypto_pk_free(pk);
}
+static void
+test_dir_parse_family_cert(void *arg)
+{
+ (void)arg;
+ ed25519_keypair_t kp_family;
+ ed25519_keypair_t kp_id;
+ char family_b64[ED25519_BASE64_LEN+1];
+ tor_cert_t *cert = NULL;
+ int r;
+
+ time_t now = 1739288377;
+ time_t lifetime = 86400;
+ time_t got_expiration = -1;
+ char *got_family_id = NULL;
+ char *expect_family_id = NULL;
+
+ setup_capture_of_logs(LOG_WARN);
+
+ ed25519_keypair_generate(&kp_family, 0);
+ ed25519_keypair_generate(&kp_id, 0);
+ ed25519_public_to_base64(family_b64, &kp_family.pubkey);
+ tor_asprintf(&expect_family_id, "ed25519:%s", family_b64);
+
+ // Wrong type.
+ cert = tor_cert_create_ed25519(&kp_family,
+ CERT_TYPE_ID_SIGNING,
+ &kp_id.pubkey,
+ now, lifetime,
+ CERT_FLAG_INCLUDE_SIGNING_KEY);
+ tt_assert(cert);
+ r = check_one_family_cert(cert->encoded, cert->encoded_len,
+ &kp_id.pubkey,
+ &got_family_id,
+ &got_expiration);
+ tt_ptr_op(got_family_id, OP_EQ, NULL);
+ tt_int_op(r, OP_EQ, -1);
+ expect_single_log_msg_containing("Wrong cert type");
+ mock_clean_saved_logs();
+ tor_cert_free(cert);
+
+ // Family key not included.
+ cert = tor_cert_create_ed25519(&kp_family,
+ CERT_TYPE_FAMILY_V_IDENTITY,
+ &kp_id.pubkey,
+ now, lifetime,
+ 0);
+ tt_assert(cert);
+ r = check_one_family_cert(cert->encoded, cert->encoded_len,
+ &kp_id.pubkey,
+ &got_family_id,
+ &got_expiration);
+ tt_ptr_op(got_family_id, OP_EQ, NULL);
+ tt_int_op(r, OP_EQ, -1);
+ expect_single_log_msg_containing("Missing family key");
+ mock_clean_saved_logs();
+ tor_cert_free(cert);
+
+ // Certified key isn't correct
+ cert = tor_cert_create_ed25519(&kp_family,
+ CERT_TYPE_FAMILY_V_IDENTITY,
+ &kp_family.pubkey,
+ now, lifetime,
+ CERT_FLAG_INCLUDE_SIGNING_KEY);
+ tt_assert(cert);
+ r = check_one_family_cert(cert->encoded, cert->encoded_len,
+ &kp_id.pubkey,
+ &got_family_id,
+ &got_expiration);
+ tt_ptr_op(got_family_id, OP_EQ, NULL);
+ tt_int_op(r, OP_EQ, -1);
+ expect_single_log_msg_containing("Key mismatch");
+ mock_clean_saved_logs();
+ tor_cert_free(cert);
+
+ // Signature is bogus.
+ cert = tor_cert_create_ed25519(&kp_family,
+ CERT_TYPE_FAMILY_V_IDENTITY,
+ &kp_id.pubkey,
+ now, lifetime,
+ CERT_FLAG_INCLUDE_SIGNING_KEY);
+ tt_assert(cert);
+ cert->encoded[cert->encoded_len-1] ^= 0x77; // corrupt the signature
+ r = check_one_family_cert(cert->encoded, cert->encoded_len,
+ &kp_id.pubkey,
+ &got_family_id,
+ &got_expiration);
+ tt_ptr_op(got_family_id, OP_EQ, NULL);
+ tt_int_op(r, OP_EQ, -1);
+ expect_single_log_msg_containing("Invalid signature");
+ mock_clean_saved_logs();
+ tor_cert_free(cert);
+
+ // Everything is okay!
+ cert = tor_cert_create_ed25519(&kp_family,
+ CERT_TYPE_FAMILY_V_IDENTITY,
+ &kp_id.pubkey,
+ now, lifetime,
+ CERT_FLAG_INCLUDE_SIGNING_KEY);
+ tt_assert(cert);
+ got_expiration = -1;
+ r = check_one_family_cert(cert->encoded, cert->encoded_len,
+ &kp_id.pubkey,
+ &got_family_id,
+ &got_expiration);
+ expect_no_log_entry();
+ tt_int_op(r, OP_EQ, 0);
+ tt_int_op(got_expiration, OP_NE, -1);
+ // Cert expirations have 1-hour granularity
+ tt_int_op(got_expiration, OP_GE, now + lifetime);
+ tt_int_op(got_expiration, OP_LT, now + lifetime + 3601);
+ tt_str_op(got_family_id, OP_EQ, expect_family_id);
+ tt_assert(!strchr(got_family_id, '=')); // not family
+
+ done:
+ tor_cert_free(cert);
+ tor_free(got_family_id);
+ tor_free(expect_family_id);
+ teardown_capture_of_logs();
+}
+
#ifndef COCCI
#define DIR_LEGACY(name) \
{ #name, test_dir_ ## name , TT_FORK, NULL, NULL }
@@ -7354,5 +7503,6 @@ struct testcase_t dir_tests[] = {
DIR(dirserv_router_get_status, TT_FORK),
DIR(dirserv_would_reject_router, TT_FORK),
DIR(dirserv_add_own_fingerprint, TT_FORK),
+ DIR(parse_family_cert, TT_FORK),
END_OF_TESTCASES
};
diff --git a/src/test/test_microdesc.c b/src/test/test_microdesc.c
@@ -378,6 +378,21 @@ static const char test_md2_withfamily_33[] =
"p accept 1-65535\n"
"id ed25519 J5lkRqyL6qW+CpN3E4RIlgJZeLgwjtmOOrjZvVhuwLQ\n";
+static const char test_md2_withfamilyids_35[] =
+ "onion-key\n"
+ "-----BEGIN RSA PUBLIC KEY-----\n"
+ "MIGJAoGBAMvEJ/JVNK7I38PPWhQMuCgkET/ki4WIas4tj5Kmqfb9kHqxMR+EunRD\n"
+ "83k4pel1yB7QdV+iTd/4SZOI8RpZP+BO1KnOTWfpztAU1lDGr19/PwdwcHaILpBD\n"
+ "nNzm6otk4/bKUQ0vqpOfJljtg0DfAm4uMAQ6BMFy6uEAF7+JupuPAgMBAAE=\n"
+ "-----END RSA PUBLIC KEY-----\n"
+ "ntor-onion-key FChIfm77vrWB7JsxQ+jMbN6VSSp1P0DYbw/2aqey4iA\n"
+ "family !Strange $D219590AC9513BCDEBBA9AB721007A4CC01BBAE3 othernode\n"
+ "family-ids "
+ "ed25519:YWxsIGhhcHB5IGZhbWlsaWVzIGFyZSBhbGlrZSAtTFQ "
+ "rlwe:0YHRh9Cw0YHRgtC70LjQstGL0LUg0YHQtdC80YzQuC0\n"
+ "p accept 1-65535\n"
+ "id ed25519 J5lkRqyL6qW+CpN3E4RIlgJZeLgwjtmOOrjZvVhuwLQ\n";
+
static void
test_md_generate(void *arg)
{
@@ -395,6 +410,16 @@ test_md_generate(void *arg)
md = dirvote_create_microdescriptor(ri, 33);
tt_str_op(md->body, OP_EQ, test_md2_withfamily_33);
+ // Try family-ids.
+ microdesc_free(md);
+ ri->family_ids = smartlist_new();
+ smartlist_add_strdup(ri->family_ids,
+ "ed25519:YWxsIGhhcHB5IGZhbWlsaWVzIGFyZSBhbGlrZSAtTFQ");
+ smartlist_add_strdup(ri->family_ids,
+ "rlwe:0YHRh9Cw0YHRgtC70LjQstGL0LUg0YHQtdC80YzQuC0");
+ md = dirvote_create_microdescriptor(ri, 35);
+ tt_str_op(md->body, OP_EQ, test_md2_withfamilyids_35);
+
done:
microdesc_free(md);
routerinfo_free(ri);
@@ -789,6 +814,44 @@ test_md_parse_no_onion_key(void *arg)
teardown_capture_of_logs();
}
+static void
+test_md_parse_family_ids(void *arg)
+{
+ (void)arg;
+
+ const char GOOD_MDS[] =
+ "onion-key\n"
+ "ntor-onion-key VHlycmFueSwgbGlrZSBoZWxsLCBpcyBub3QgZWFzaWw\n"
+ "id ed25519 eSBjb25xdWVyZWQ7IHlldCB3ZSBoYXZlIHRoaXMgY28\n"
+ "family-ids\n"
+ "onion-key\n"
+ "ntor-onion-key bnNvbGF0aW9uIHdpdGggdXMsIHRoYXQgdGhlIGhhcmQ\n"
+ "id ed25519 ZXIgdGhlIGNvbmZsaWN0LCB0aGUgbW9yZSBnbG9yaW8\n"
+ "family-ids ed25519:dXMgdGhlIHRyaXVtcGguICAgIC1UaG9tYXMgUGFpbmU "
+ "other:Example\n";
+ smartlist_t *mds = NULL;
+ mds = microdescs_parse_from_string(GOOD_MDS, NULL, 1, SAVED_NOWHERE, NULL);
+ tt_assert(mds);
+ tt_int_op(smartlist_len(mds), OP_EQ, 2);
+
+ const microdesc_t *md1 = smartlist_get(mds, 0);
+ tt_ptr_op(md1->family_ids, OP_EQ, NULL);
+
+ const microdesc_t *md2 = smartlist_get(mds, 1);
+ tt_ptr_op(md2->family_ids, OP_NE, NULL);
+ tt_int_op(smartlist_len(md2->family_ids), OP_EQ, 2);
+ tt_str_op(smartlist_get(md2->family_ids, 0), OP_EQ,
+ "ed25519:dXMgdGhlIHRyaXVtcGguICAgIC1UaG9tYXMgUGFpbmU");
+ tt_str_op(smartlist_get(md2->family_ids, 1), OP_EQ,
+ "other:Example");
+
+ done:
+ if (mds) {
+ SMARTLIST_FOREACH(mds, microdesc_t *, m, microdesc_free(m));
+ smartlist_free(mds);
+ }
+}
+
static int mock_rgsbd_called = 0;
static routerstatus_t *mock_rgsbd_val_a = NULL;
static routerstatus_t *mock_rgsbd_val_b = NULL;
@@ -924,6 +987,7 @@ struct testcase_t microdesc_tests[] = {
{ "parse", test_md_parse, 0, NULL, NULL },
{ "parse_id_ed25519", test_md_parse_id_ed25519, 0, NULL, NULL },
{ "parse_no_onion_key", test_md_parse_no_onion_key, 0, NULL, NULL },
+ { "parse_family_ids", test_md_parse_family_ids, 0, NULL, NULL },
{ "reject_cache", test_md_reject_cache, TT_FORK, NULL, NULL },
{ "corrupt_desc", test_md_corrupt_desc, TT_FORK, NULL, NULL },
END_OF_TESTCASES
diff --git a/src/test/test_nodelist.c b/src/test/test_nodelist.c
@@ -559,34 +559,34 @@ test_nodelist_node_nodefamily(void *arg)
memcpy(mock_node2.identity, "SecondNodeWe'reTestn", DIGEST_LEN);
// empty families.
- tt_assert(! node_family_contains(&mock_node1, &mock_node2));
- tt_assert(! node_family_contains(&mock_node2, &mock_node1));
+ tt_assert(! node_family_list_contains(&mock_node1, &mock_node2));
+ tt_assert(! node_family_list_contains(&mock_node2, &mock_node1));
// Families contain nodes, but not these nodes
mock_ri.declared_family = smartlist_new();
smartlist_add(mock_ri.declared_family, (char*)"NodeThree");
mock_md.family = nodefamily_parse("NodeFour", NULL, 0);
- tt_assert(! node_family_contains(&mock_node1, &mock_node2));
- tt_assert(! node_family_contains(&mock_node2, &mock_node1));
+ tt_assert(! node_family_list_contains(&mock_node1, &mock_node2));
+ tt_assert(! node_family_list_contains(&mock_node2, &mock_node1));
// Families contain one another.
smartlist_add(mock_ri.declared_family, (char*)
"4e6f64654f6e654e6f6465314e6f64654f6e6531");
- tt_assert(! node_family_contains(&mock_node1, &mock_node2));
- tt_assert(node_family_contains(&mock_node2, &mock_node1));
+ tt_assert(! node_family_list_contains(&mock_node1, &mock_node2));
+ tt_assert(node_family_list_contains(&mock_node2, &mock_node1));
nodefamily_free(mock_md.family);
mock_md.family = nodefamily_parse(
"NodeFour "
"5365636f6e644e6f64655765277265546573746e", NULL, 0);
- tt_assert(node_family_contains(&mock_node1, &mock_node2));
- tt_assert(node_family_contains(&mock_node2, &mock_node1));
+ tt_assert(node_family_list_contains(&mock_node1, &mock_node2));
+ tt_assert(node_family_list_contains(&mock_node2, &mock_node1));
// Try looking up families now.
MOCK(node_get_by_nickname, mock_node_get_by_nickname);
MOCK(node_get_by_id, mock_node_get_by_id);
- node_lookup_declared_family(nodes, &mock_node1);
+ node_lookup_declared_family_list(nodes, &mock_node1);
tt_int_op(smartlist_len(nodes), OP_EQ, 2);
const node_t *n = smartlist_get(nodes, 0);
tt_mem_op(n->identity, OP_EQ, "SecondNodeWe'reTestn", DIGEST_LEN);
@@ -597,7 +597,7 @@ test_nodelist_node_nodefamily(void *arg)
SMARTLIST_FOREACH(nodes, node_t *, x, tor_free(x));
smartlist_clear(nodes);
- node_lookup_declared_family(nodes, &mock_node2);
+ node_lookup_declared_family_list(nodes, &mock_node2);
tt_int_op(smartlist_len(nodes), OP_EQ, 2);
n = smartlist_get(nodes, 0);
// This gets a truncated hex hex ID since it was looked up by name
diff --git a/src/test/test_routerkeys.c b/src/test/test_routerkeys.c
@@ -5,6 +5,7 @@
#include "orconfig.h"
#define ROUTER_PRIVATE
+#define ROUTERKEYS_PRIVATE
#include "core/or/or.h"
#include "app/config/config.h"
#include "feature/relay/router.h"
@@ -735,6 +736,87 @@ test_routerkeys_rsa_ed_crosscert(void *arg)
tor_free(cc);
}
+static void
+test_routerkeys_family_key_fname(void *arg)
+{
+ (void)arg;
+
+ tt_assert(is_family_key_fname("hello.secret_family_key"));
+ tt_assert(is_family_key_fname("xyzzy.secret_family_key"));
+ tt_assert(is_family_key_fname("909.secret_family_key"));
+ tt_assert(! is_family_key_fname("zzz.secret_family_key~"));
+ tt_assert(! is_family_key_fname("secret_family_key"));
+
+ done:
+ ;
+}
+
+static void
+test_routerkeys_load_family_keys(void *arg)
+{
+ (void) arg;
+ char *dname = tor_strdup(get_fname_rnd("fkeys"));
+ char *fname = NULL;
+ or_options_t *options = get_options_mutable();
+ ed25519_public_key_t pubkey;
+
+#ifdef _WIN32
+ tt_assert(0==mkdir(dname));
+#else
+ tt_assert(0==mkdir(dname,0700));
+#endif
+
+ options->FamilyIds = smartlist_new();
+
+ // Not a family key, will be ignored
+ tor_asprintf(&fname, "%s"PATH_SEPARATOR"junk.1", dname);
+ write_str_to_file(fname, "hello world", 0);
+ tor_free(fname);
+
+ tt_int_op(0, OP_EQ, load_family_id_keys_impl(options, dname));
+ tt_int_op(0, OP_EQ, smartlist_len(get_current_family_id_keys()));
+
+ // Create a family key; make sure we can load it.
+ tor_asprintf(&fname, "%s"PATH_SEPARATOR"cg.secret_family_key", dname);
+ tt_int_op(0, OP_EQ, create_family_id_key(fname, &pubkey));
+ tor_free(fname);
+ smartlist_add(options->FamilyIds, tor_memdup(&pubkey, sizeof(pubkey)));
+
+ tt_int_op(0, OP_EQ, load_family_id_keys_impl(options, dname));
+ tt_int_op(1, OP_EQ, smartlist_len(get_current_family_id_keys()));
+
+ //Try a second key.
+ tor_asprintf(&fname, "%s"PATH_SEPARATOR"eb.secret_family_key", dname);
+ tt_int_op(0, OP_EQ, create_family_id_key(fname, &pubkey));
+ smartlist_add(options->FamilyIds, tor_memdup(&pubkey, sizeof(pubkey)));
+ tor_free(fname);
+
+ tt_int_op(0, OP_EQ, load_family_id_keys_impl(options, dname));
+ tt_int_op(2, OP_EQ, smartlist_len(get_current_family_id_keys()));
+
+ // Try an unlisted key, make sure it isn't loaded.
+ tor_asprintf(&fname, "%s"PATH_SEPARATOR"gt.secret_family_key", dname);
+ tt_int_op(0, OP_EQ, create_family_id_key(fname, &pubkey));
+ // Do not add to FamilyIDs here; we're leaving it unlisted.
+ tor_free(fname);
+
+ tt_int_op(0, OP_EQ, load_family_id_keys_impl(options, dname));
+ tt_int_op(2, OP_EQ, smartlist_len(get_current_family_id_keys()));
+
+ // Make a junk key, make sure it causes an error.
+ tor_asprintf(&fname, "%s"PATH_SEPARATOR"xyz.secret_family_key", dname);
+ write_str_to_file(fname, "hello world", 0);
+ tor_free(fname);
+
+ tt_int_op(-1, OP_EQ, load_family_id_keys_impl(options, dname));
+ // keys unchanged
+ tt_int_op(2, OP_EQ, smartlist_len(get_current_family_id_keys()));
+
+ done:
+ tor_free(dname);
+ tor_free(fname);
+}
+
#define TEST(name, flags) \
{ #name , test_routerkeys_ ## name, (flags), NULL, NULL }
@@ -749,5 +831,7 @@ struct testcase_t routerkeys_tests[] = {
TEST(cross_certify_ntor, 0),
TEST(cross_certify_tap, 0),
TEST(rsa_ed_crosscert, 0),
+ TEST(family_key_fname, 0),
+ TEST(load_family_keys, TT_FORK),
END_OF_TESTCASES
};