tor

The Tor anonymity network
git clone https://git.dasho.dev/tor.git
Log | Files | Refs | README | LICENSE

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:
Achanges/happy-families | 17+++++++++++++++++
Achanges/happy-families-client | 4++++
Mdoc/man/tor.1.txt | 33+++++++++++++++++++++++++++++++++
Msrc/app/config/config.c | 9+++++++++
Msrc/app/config/or_options_st.h | 4++++
Msrc/app/config/tor_cmdline_mode.h | 1+
Msrc/app/main/main.c | 41+++++++++++++++++++++++++++++++++++++++++
Msrc/app/main/ntmain.c | 1+
Msrc/core/or/protover.c | 4++--
Msrc/feature/dirauth/dirvote.c | 9+++++++++
Msrc/feature/dirauth/dirvote.h | 8+++++++-
Msrc/feature/dirparse/microdesc_parse.c | 11+++++++++++
Msrc/feature/dirparse/parsecommon.h | 2++
Msrc/feature/dirparse/routerparse.c | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/feature/dirparse/routerparse.h | 10++++++++++
Msrc/feature/nodelist/microdesc.c | 4++++
Msrc/feature/nodelist/microdesc_st.h | 6++++++
Msrc/feature/nodelist/nodelist.c | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/feature/nodelist/nodelist.h | 7++++---
Msrc/feature/nodelist/routerinfo_st.h | 5+++++
Msrc/feature/nodelist/routerlist.c | 4++++
Msrc/feature/nodelist/torcert.h | 1+
Msrc/feature/relay/relay_config.c | 15+++++++++++++++
Msrc/feature/relay/router.c | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/feature/relay/router.h | 2++
Msrc/feature/relay/routerkeys.c | 279+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/feature/relay/routerkeys.h | 22++++++++++++++++++++++
Msrc/test/test_dir.c | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/test/test_microdesc.c | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/test/test_nodelist.c | 20++++++++++----------
Msrc/test/test_routerkeys.c | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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 };