Commit 65759f29 authored by David Goulet's avatar David Goulet
Browse files

Merge branch 'tor-github/pr/1563'

parents df6c5382 12305b6b
......@@ -73,7 +73,6 @@ const control_cmd_syntax_t onion_client_auth_add_syntax = {
* register the new client-side client auth credentials:
* "ONION_CLIENT_AUTH_ADD" SP HSAddress
* SP KeyType ":" PrivateKeyBlob
* [SP "ClientName=" Nickname]
* [SP "Type=" TYPE] CRLF
*/
int
......@@ -112,14 +111,7 @@ handle_control_onion_client_auth_add(control_connection_t *conn,
/* Now let's parse the remaining arguments (variable size) */
for (const config_line_t *line = args->kwargs; line; line = line->next) {
if (!strcasecmp(line->key, "ClientName")) {
if (strlen(line->value) > HS_CLIENT_AUTH_MAX_NICKNAME_LENGTH) {
control_write_endreply(conn, 512, "Too big 'ClientName' argument");
goto err;
}
creds->nickname = tor_strdup(line->value);
} else if (!strcasecmpstart(line->key, "Flags")) {
if (!strcasecmpstart(line->key, "Flags")) {
smartlist_split_string(flags, line->value, ",", SPLIT_IGNORE_BLANK, 0);
if (smartlist_len(flags) < 1) {
control_write_endreply(conn, 512, "Invalid 'Flags' argument");
......@@ -145,6 +137,10 @@ handle_control_onion_client_auth_add(control_connection_t *conn,
/* It's a bug because the service addr has already been validated above */
control_printf_endreply(conn, 512, "Invalid v3 address \"%s\"", hsaddress);
break;
case REGISTER_FAIL_PERMANENT_STORAGE:
control_printf_endreply(conn, 553, "Unable to store creds for \"%s\"",
hsaddress);
break;
case REGISTER_SUCCESS_ALREADY_EXISTS:
control_printf_endreply(conn, 251,"Client for onion existed and replaced");
break;
......@@ -245,10 +241,6 @@ encode_client_auth_cred_for_control_port(
smartlist_add_asprintf(control_line, "CLIENT x25519:%s", x25519_b64);
if (cred->nickname) { /* nickname is optional */
smartlist_add_asprintf(control_line, " ClientName=%s", cred->nickname);
}
if (cred->flags) { /* flags are also optional */
if (cred->flags & CLIENT_AUTH_FLAG_IS_PERMANENT) {
smartlist_add_asprintf(control_line, " Flags=Permanent");
......
......@@ -1445,6 +1445,80 @@ client_dir_fetch_unexpected(dir_connection_t *dir_conn, const char *reason,
NULL);
}
/** Get the full filename for storing the client auth credentials for the
* service in <b>onion_address</b>. The base directory is <b>dir</b>.
* This function never returns NULL. */
static char *
get_client_auth_creds_filename(const char *onion_address,
const char *dir)
{
char *full_fname = NULL;
char *fname;
tor_asprintf(&fname, "%s.auth_private", onion_address);
full_fname = hs_path_from_filename(dir, fname);
tor_free(fname);
return full_fname;
}
/** Permanently store the credentials in <b>creds</b> to disk.
*
* Return -1 if there was an error while storing the credentials, otherwise
* return 0.
*/
static int
store_permanent_client_auth_credentials(
const hs_client_service_authorization_t *creds)
{
const or_options_t *options = get_options();
char *full_fname = NULL;
char *file_contents = NULL;
char priv_key_b32[BASE32_NOPAD_LEN(CURVE25519_PUBKEY_LEN)+1];
int retval = -1;
tor_assert(creds->flags & CLIENT_AUTH_FLAG_IS_PERMANENT);
/* We need ClientOnionAuthDir to be set, otherwise we can't proceed */
if (!options->ClientOnionAuthDir) {
log_warn(LD_GENERAL, "Can't register permanent client auth credentials "
"for %s without ClientOnionAuthDir option. Discarding.",
creds->onion_address);
goto err;
}
/* Make sure the directory exists and is private enough. */
if (check_private_dir(options->ClientOnionAuthDir, 0, options->User) < 0) {
goto err;
}
/* Get filename that we should store the credentials */
full_fname = get_client_auth_creds_filename(creds->onion_address,
options->ClientOnionAuthDir);
/* Encode client private key */
base32_encode(priv_key_b32, sizeof(priv_key_b32),
(char*)creds->enc_seckey.secret_key,
sizeof(creds->enc_seckey.secret_key));
/* Get the full file contents and write it to disk! */
tor_asprintf(&file_contents, "%s:descriptor:x25519:%s",
creds->onion_address, priv_key_b32);
if (write_str_to_file(full_fname, file_contents, 0) < 0) {
log_warn(LD_GENERAL, "Failed to write client auth creds file for %s!",
creds->onion_address);
goto err;
}
retval = 0;
err:
tor_free(file_contents);
tor_free(full_fname);
return retval;
}
/** Register the credential <b>creds</b> as part of the client auth subsystem.
*
* Takes ownership of <b>creds</b>.
......@@ -1468,6 +1542,15 @@ hs_client_register_auth_credentials(hs_client_service_authorization_t *creds)
return REGISTER_FAIL_BAD_ADDRESS;
}
/* If we reach this point, the credentials will be stored one way or another:
* Make them permanent if the user asked us to. */
if (creds->flags & CLIENT_AUTH_FLAG_IS_PERMANENT) {
if (store_permanent_client_auth_credentials(creds) < 0) {
client_service_authorization_free(creds);
return REGISTER_FAIL_PERMANENT_STORAGE;
}
}
old_creds = digest256map_get(client_auths, service_identity_pk.pubkey);
if (old_creds) {
digest256map_remove(client_auths, service_identity_pk.pubkey);
......@@ -1486,6 +1569,128 @@ hs_client_register_auth_credentials(hs_client_service_authorization_t *creds)
return retval;
}
/** Load a client authorization file with <b>filename</b> that is stored under
* the global client auth directory, and return a newly-allocated credentials
* object if it parsed well. Otherwise, return NULL.
*/
static hs_client_service_authorization_t *
get_creds_from_client_auth_filename(const char *filename,
const or_options_t *options)
{
hs_client_service_authorization_t *auth = NULL;
char *client_key_file_path = NULL;
char *client_key_str = NULL;
log_info(LD_REND, "Loading a client authorization key file %s...",
filename);
if (!auth_key_filename_is_valid(filename)) {
log_notice(LD_REND, "Client authorization unrecognized filename %s. "
"File must end in .auth_private. Ignoring.",
filename);
goto err;
}
/* Create a full path for a file. */
client_key_file_path = hs_path_from_filename(options->ClientOnionAuthDir,
filename);
client_key_str = read_file_to_str(client_key_file_path, 0, NULL);
if (!client_key_str) {
log_warn(LD_REND, "The file %s cannot be read.", filename);
goto err;
}
auth = parse_auth_file_content(client_key_str);
if (!auth) {
goto err;
}
err:
tor_free(client_key_str);
tor_free(client_key_file_path);
return auth;
}
/*
* Remove the file in <b>filename</b> under the global client auth credential
* storage.
*/
static void
remove_client_auth_creds_file(const char *filename)
{
char *creds_file_path = NULL;
const or_options_t *options = get_options();
creds_file_path = hs_path_from_filename(options->ClientOnionAuthDir,
filename);
if (tor_unlink(creds_file_path) != 0) {
log_warn(LD_REND, "Failed to remove client auth file (%s).",
creds_file_path);
goto end;
}
log_warn(LD_REND, "Successfuly removed client auth file (%s).",
creds_file_path);
end:
tor_free(creds_file_path);
}
/**
* Find the filesystem file corresponding to the permanent client auth
* credentials in <b>cred</b> and remove it.
*/
static void
find_and_remove_client_auth_creds_file(
const hs_client_service_authorization_t *cred)
{
smartlist_t *file_list = NULL;
const or_options_t *options = get_options();
tor_assert(cred->flags & CLIENT_AUTH_FLAG_IS_PERMANENT);
if (!options->ClientOnionAuthDir) {
log_warn(LD_REND, "Found permanent credential but no ClientOnionAuthDir "
"configured. There is no file to be removed.");
goto end;
}
file_list = tor_listdir(options->ClientOnionAuthDir);
if (file_list == NULL) {
log_warn(LD_REND, "Client authorization key directory %s can't be listed.",
options->ClientOnionAuthDir);
goto end;
}
SMARTLIST_FOREACH_BEGIN(file_list, const char *, filename) {
hs_client_service_authorization_t *tmp_cred = NULL;
tmp_cred = get_creds_from_client_auth_filename(filename, options);
if (!tmp_cred) {
continue;
}
/* Find the right file for this credential */
if (!strcmp(tmp_cred->onion_address, cred->onion_address)) {
/* Found it! Remove the file! */
remove_client_auth_creds_file(filename);
/* cleanup and get out of here */
client_service_authorization_free(tmp_cred);
break;
}
client_service_authorization_free(tmp_cred);
} SMARTLIST_FOREACH_END(filename);
end:
if (file_list) {
SMARTLIST_FOREACH(file_list, char *, s, tor_free(s));
smartlist_free(file_list);
}
}
/** Remove client auth credentials for the service <b>hs_address</b>. */
hs_client_removal_auth_status_t
hs_client_remove_auth_credentials(const char *hsaddress)
......@@ -1502,8 +1707,14 @@ hs_client_remove_auth_credentials(const char *hsaddress)
hs_client_service_authorization_t *cred = NULL;
cred = digest256map_remove(client_auths, service_identity_pk.pubkey);
/* digestmap_remove() returns the previously stored data if there were any */
if (cred) {
if (cred->flags & CLIENT_AUTH_FLAG_IS_PERMANENT) {
/* These creds are stored on disk: remove the corresponding file. */
find_and_remove_client_auth_creds_file(cred);
}
client_service_authorization_free(cred);
return REMOVAL_SUCCESS;
}
......@@ -1799,10 +2010,6 @@ client_service_authorization_free_(hs_client_service_authorization_t *auth)
return;
}
if (auth->nickname) {
tor_free(auth->nickname);
}
memwipe(auth, 0, sizeof(*auth));
tor_free(auth);
}
......@@ -1845,6 +2052,13 @@ auth_key_filename_is_valid(const char *filename)
return ret;
}
/** Parse the client auth credentials off a string in <b>client_key_str</b>
* based on the file format documented in the "Client side configuration"
* section of rend-spec-v3.txt.
*
* Return NULL if there was an error, otherwise return a newly allocated
* hs_client_service_authorization_t structure.
*/
STATIC hs_client_service_authorization_t *
parse_auth_file_content(const char *client_key_str)
{
......@@ -1875,7 +2089,7 @@ parse_auth_file_content(const char *client_key_str)
goto err;
}
if (strlen(seckey_b32) != BASE32_NOPAD_LEN(CURVE25519_PUBKEY_LEN)) {
if (strlen(seckey_b32) != BASE32_NOPAD_LEN(CURVE25519_SECKEY_LEN)) {
log_warn(LD_REND, "Client authorization encoded base32 private key "
"length is invalid: %s", seckey_b32);
goto err;
......@@ -1892,6 +2106,9 @@ parse_auth_file_content(const char *client_key_str)
}
strncpy(auth->onion_address, onion_address, HS_SERVICE_ADDR_LEN_BASE32);
/* We are reading this from the disk, so set the permanent flag anyway. */
auth->flags |= CLIENT_AUTH_FLAG_IS_PERMANENT;
/* Success. */
goto done;
......@@ -1918,10 +2135,7 @@ hs_config_client_authorization(const or_options_t *options,
{
int ret = -1;
digest256map_t *auths = digest256map_new();
char *key_dir = NULL;
smartlist_t *file_list = NULL;
char *client_key_str = NULL;
char *client_key_file_path = NULL;
tor_assert(options);
......@@ -1932,82 +2146,54 @@ hs_config_client_authorization(const or_options_t *options,
goto end;
}
key_dir = tor_strdup(options->ClientOnionAuthDir);
/* Make sure the directory exists and is private enough. */
if (check_private_dir(key_dir, 0, options->User) < 0) {
if (check_private_dir(options->ClientOnionAuthDir, 0, options->User) < 0) {
goto end;
}
file_list = tor_listdir(key_dir);
file_list = tor_listdir(options->ClientOnionAuthDir);
if (file_list == NULL) {
log_warn(LD_REND, "Client authorization key directory %s can't be listed.",
key_dir);
options->ClientOnionAuthDir);
goto end;
}
SMARTLIST_FOREACH_BEGIN(file_list, char *, filename) {
SMARTLIST_FOREACH_BEGIN(file_list, const char *, filename) {
hs_client_service_authorization_t *auth = NULL;
ed25519_public_key_t identity_pk;
log_info(LD_REND, "Loading a client authorization key file %s...",
filename);
if (!auth_key_filename_is_valid(filename)) {
log_notice(LD_REND, "Client authorization unrecognized filename %s. "
"File must end in .auth_private. Ignoring.",
filename);
auth = get_creds_from_client_auth_filename(filename, options);
if (!auth) {
continue;
}
/* Create a full path for a file. */
client_key_file_path = hs_path_from_filename(key_dir, filename);
client_key_str = read_file_to_str(client_key_file_path, 0, NULL);
/* Free the file path immediately after using it. */
tor_free(client_key_file_path);
/* If we cannot read the file, continue with the next file. */
if (!client_key_str) {
log_warn(LD_REND, "The file %s cannot be read.", filename);
/* Parse the onion address to get an identity public key and use it
* as a key of global map in the future. */
if (hs_parse_address(auth->onion_address, &identity_pk,
NULL, NULL) < 0) {
log_warn(LD_REND, "The onion address \"%s\" is invalid in "
"file %s", filename, auth->onion_address);
client_service_authorization_free(auth);
continue;
}
auth = parse_auth_file_content(client_key_str);
/* Free immediately after using it. */
tor_free(client_key_str);
if (auth) {
/* Parse the onion address to get an identity public key and use it
* as a key of global map in the future. */
if (hs_parse_address(auth->onion_address, &identity_pk,
NULL, NULL) < 0) {
log_warn(LD_REND, "The onion address \"%s\" is invalid in "
"file %s", filename, auth->onion_address);
client_service_authorization_free(auth);
continue;
}
if (digest256map_get(auths, identity_pk.pubkey)) {
if (digest256map_get(auths, identity_pk.pubkey)) {
log_warn(LD_REND, "Duplicate authorization for the same hidden "
"service address %s.",
"service address %s.",
safe_str_client_opts(options, auth->onion_address));
client_service_authorization_free(auth);
goto end;
}
digest256map_set(auths, identity_pk.pubkey, auth);
log_info(LD_REND, "Loaded a client authorization key file %s.",
filename);
}
digest256map_set(auths, identity_pk.pubkey, auth);
log_info(LD_REND, "Loaded a client authorization key file %s.",
filename);
} SMARTLIST_FOREACH_END(filename);
/* Success. */
ret = 0;
end:
tor_free(key_dir);
tor_free(client_key_str);
tor_free(client_key_file_path);
if (file_list) {
SMARTLIST_FOREACH(file_list, char *, s, tor_free(s));
smartlist_free(file_list);
......
......@@ -45,6 +45,8 @@ typedef enum {
REGISTER_SUCCESS_AND_DECRYPTED,
/* We failed to register these credentials, because of a bad HS address. */
REGISTER_FAIL_BAD_ADDRESS,
/* We failed to register these credentials, because of a bad HS address. */
REGISTER_FAIL_PERMANENT_STORAGE,
} hs_client_register_auth_status_t;
/* Status code of client auth credential removal */
......@@ -60,9 +62,6 @@ typedef enum {
/** Flag to set when a client auth is permanent (saved on disk). */
#define CLIENT_AUTH_FLAG_IS_PERMANENT (1<<0)
/** Max length of a client auth nickname */
#define HS_CLIENT_AUTH_MAX_NICKNAME_LENGTH 255
/** Client-side configuration of client authorization */
typedef struct hs_client_service_authorization_t {
/** An curve25519 secret key used to compute decryption keys that
......@@ -72,9 +71,6 @@ typedef struct hs_client_service_authorization_t {
/** An onion address that is used to connect to the onion service. */
char onion_address[HS_SERVICE_ADDR_LEN_BASE32+1];
/* An optional nickname for this client */
char *nickname;
/* Optional flags for this client. */
int flags;
} hs_client_service_authorization_t;
......
......@@ -30,6 +30,17 @@
#include "test/test_helpers.h"
#ifdef HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif
#ifdef _WIN32
/* For mkdir() */
#include <direct.h>
#else
#include <dirent.h>
#endif /* defined(_WIN32) */
/* mock ID digest and longname for node that's in nodelist */
#define HSDIR_EXIST_ID \
"\xAA\xAA\xAA\xAA\xAA\xAA\xAA\xAA\xAA\xAA" \
......@@ -234,8 +245,7 @@ test_hs_control_good_onion_client_auth_add(void *arg)
/* Register first service */
args = tor_strdup("2fvhjskjet3n5syd6yfg5lhvwcs62bojmthr35ko5bllr3iqdb4ctdyd "
"x25519:iJ1tjKCrMAbiFT2bVrCjhbfMDnE1fpaRbIS5ZHKUvEQ= "
"ClientName=bob Flags=Permanent");
"x25519:iJ1tjKCrMAbiFT2bVrCjhbfMDnE1fpaRbIS5ZHKUvEQ= ");
retval = handle_control_command(&conn, (uint32_t) strlen(args), args);
tt_int_op(retval, OP_EQ, 0);
......@@ -266,13 +276,11 @@ test_hs_control_good_onion_client_auth_add(void *arg)
hs_client_service_authorization_t *client_2fv =
digest256map_get(client_auths, service_identity_pk_2fv.pubkey);
tt_assert(client_2fv);
tt_str_op(client_2fv->nickname, OP_EQ, "bob");
tt_int_op(client_2fv->flags, OP_EQ, CLIENT_AUTH_FLAG_IS_PERMANENT);
tt_int_op(client_2fv->flags, OP_EQ, 0);
hs_client_service_authorization_t *client_jt4 =
digest256map_get(client_auths, service_identity_pk_jt4.pubkey);
tt_assert(client_jt4);
tt_assert(!client_jt4->nickname);
tt_int_op(client_jt4->flags, OP_EQ, 0);
/* Now let's VIEW the auth credentials */
......@@ -285,8 +293,7 @@ test_hs_control_good_onion_client_auth_add(void *arg)
#define VIEW_CORRECT_REPLY_NO_ADDR "250-ONION_CLIENT_AUTH_VIEW\r\n" \
"250-CLIENT x25519:eIIdIGoSZwI2Q/lSzpf92akGki5I+PZIDz37MA5BhlA=\r\n"\
"250-CLIENT x25519:iJ1tjKCrMAbiFT2bVrCjhbfMDnE1fpaRbIS5ZHKUvEQ= " \
"ClientName=bob Flags=Permanent\r\n" \
"250-CLIENT x25519:iJ1tjKCrMAbiFT2bVrCjhbfMDnE1fpaRbIS5ZHKUvEQ=\r\n" \
"250 OK\r\n"
retval = handle_control_command(&conn, (uint32_t) strlen(args), args);
......@@ -466,6 +473,164 @@ test_hs_control_bad_onion_client_auth_add(void *arg)
hs_client_free_all();
}
/** Test that we can correctly add permanent client auth credentials using the
* control port. */
static void
test_hs_control_store_permanent_creds(void *arg)
{
(void) arg;
MOCK(connection_write_to_buf_impl_, connection_write_to_buf_mock);
int retval;
ed25519_public_key_t service_identity_pk_2fv;
control_connection_t conn;
char *args = NULL;
char *cp1 = NULL;
char *creds_file_str = NULL;
char *creds_fname = NULL;
size_t sz;
{ /* Setup the control conn */
memset(&conn, 0, sizeof(control_connection_t));
TO_CONN(&conn)->outbuf = buf_new();
conn.current_cmd = tor_strdup("ONION_CLIENT_AUTH_ADD");
}
{ /* Setup the services */
retval = hs_parse_address(
"2fvhjskjet3n5syd6yfg5lhvwcs62bojmthr35ko5bllr3iqdb4ctdyd",
&service_identity_pk_2fv,
NULL, NULL);
tt_int_op(retval, OP_EQ, 0);
}
digest256map_t *client_auths = get_hs_client_auths_map();
tt_assert(!client_auths);
/* Try registering first service with no ClientOnionAuthDir set */
args = tor_strdup("2fvhjskjet3n5syd6yfg5lhvwcs62bojmthr35ko5bllr3iqdb4ctdyd "
"x25519:iJ1tjKCrMAbiFT2bVrCjhbfMDnE1fpaRbIS5ZHKUvEQ= "
"Flags=Permanent");
retval = handle_control_command(&conn, (uint32_t) strlen(args), args);
tt_int_op(retval, OP_EQ, 0);
/* Check control port response. This one should fail. */
cp1 = buf_get_contents(TO_CONN(&conn)->outbuf, &sz);
tt_str_op(cp1, OP_EQ, "553 Unable to store creds for "
"\"2fvhjskjet3n5syd6yfg5lhvwcs62bojmthr35ko5bllr3iqdb4ctdyd\"\r\n");
{ /* Setup ClientOnionAuthDir */
int ret;
char *perm_creds_dir = tor_strdup(get_fname("permanent_credentials"));
#ifdef _WIN32
ret = mkdir(perm_creds_dir);
#else
ret = mkdir(perm_creds_dir, 0700);
#endif
tt_int_op(ret, OP_EQ, 0);
get_options_mutable()->ClientOnionAuthDir = perm_creds_dir;
}
tor_free(args);
tor_free(cp1);
/* Try the control port command again. This time it should work! */
args = tor_strdup("2fvhjskjet3n5syd6yfg5lhvwcs62bojmthr35ko5bllr3iqdb4ctdyd "
"x25519:iJ1tjKCrMAbiFT2bVrCjhbfMDnE1fpaRbIS5ZHKUvEQ= "
"Flags=Permanent");
retval = handle_control_command(&conn, (uint32_t) strlen(args), args);
tt_int_op(retval, OP_EQ, 0);
/* Check control port response */
cp1 = buf_get_contents(TO_CONN(&conn)->outbuf, &sz);
tt_str_op(cp1, OP_EQ, "250 OK\r\n");
/* Check file contents! */
creds_fname = tor_strdup(get_fname("permanent_credentials/"
"2fvhjskjet3n5syd6yfg5lhvwcs62bojmthr35ko5bllr3iqdb4ctdyd.auth_private"));
creds_file_str = read_file_to_str(creds_fname, RFTS_BIN, NULL);
tt_assert(creds_file_str);
tt_str_op(creds_file_str, OP_EQ,
"2fvhjskjet3n5syd6yfg5lhvwcs62bojmthr35ko5bllr3iqdb4ctdyd:descriptor:"
/* This is the base32 represenation of the base64 iJ1t... key above */
"x25519:rcow3dfavmyanyqvhwnvnmfdqw34ydtrgv7jnelmqs4wi4uuxrca");
tor_free(args);
tor_free(cp1);
/* Overwrite the credentials and check that they got overwrited. */
args = tor_strdup("2fvhjskjet3n5syd6yfg5lhvwcs62bojmthr35ko5bllr3iqdb4ctdyd "
"x25519:UDRvZLvcJo0QRLvDfkpgbtsqbkhIUQZyeo2FNBrgS18= "
"Flags=Permanent");
retval = handle_control_command(&conn, (uint32_t) strlen(args), args);
tt_int_op(retval, OP_EQ, 0);
/* Check control port response: we replaced! */
cp1 = buf_get_contents(TO_CONN(&conn)->outbuf, &sz);