From 607bd3a650cdf1b0b58f945723ba23b1bb660ac8 Mon Sep 17 00:00:00 2001 From: Shane Gable <76533041+shangabl@users.noreply.github.com> Date: Fri, 12 Aug 2022 14:46:09 -0500 Subject: [PATCH] Add option for client-token header (#92) * Add option for client-token header --- README.md | 16 +++++-- src/LocalproxyConfig.h | 4 ++ src/TcpAdapterProxy.cpp | 6 ++- src/TcpAdapterProxy.h | 1 + src/main.cpp | 19 ++++++-- test/AdapterTests.cpp | 100 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 139 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f93526f..270a5f5 100644 --- a/README.md +++ b/README.md @@ -304,12 +304,22 @@ After preparing this directory, point to it when running the local proxy with th * Consider running the local proxy on separate hosts, containers, sandboxes, chroot jail, or a virtualized environment #### Access tokens - -* Access tokens are not meant to be re-used. -* After localproxy uses an access token, it will no longer be valid. +* After localproxy uses an access token, it will no longer be valid without an accompanying Client Token. * You can revoke an existing token and get a new valid token by calling [RotateTunnelAccessToken](https://docs.aws.amazon.com/iot/latest/apireference/API_iot-secure-tunneling_RotateTunnelAccessToken.html). * Refer to the [Developer Guide](https://docs.aws.amazon.com/iot/latest/developerguide/iot-secure-tunneling-troubleshooting.html) for troubleshooting connectivity issues that can be due to an invalid token. +#### Client Tokens +* The client token is an added security layer to protect the tunnel by ensuring that only the agent that generated the client token can use a particular access token to connect to a tunnel. +* Only one client token value may be present in the request. Supplying multiple values will cause the handshake to fail. +* The client token is optional. +* The client token must be unique across all the open tunnels per AWS account +* It's recommended to use a UUID to generate the client token. +* The client token can be any string that matches the regex `^[a-zA-Z0-9-]{32,128}$` +* If a client token is provided, then local proxy needs to pass the same client token for subsequent retries (This is yet to be implemented in the current version of local proxy) +* If a client token is not provided, then the access token will become invalid after a successful handshake, and localproxy won't be able to reconnect using the same access token. +* The Client Token may be passed using the **-i** argument from the command line or setting the **AWSIOT_TUNNEL_CLIENT_TOKEN** environment variable. + + ### IPv6 support The local proxy uses IPv4 and IPv6 dynamically based on how addresses are specified directly by the user, or how are they resolved on the system. For example, if 'localhost' resolves to '127.0.0.1' then IPv4 will is being used to connect or as the listening address. If localhost resolves to '::1' then IPv6 will be used. diff --git a/src/LocalproxyConfig.h b/src/LocalproxyConfig.h index fb6a8ab..bdf7b8a 100644 --- a/src/LocalproxyConfig.h +++ b/src/LocalproxyConfig.h @@ -71,6 +71,10 @@ namespace aws { */ std::string access_token { }; proxy_mode mode{ proxy_mode::UNKNOWN }; + /** + * A unique client-token to ensure only the agent which generated the token may connect to a tunnel + */ + std::string client_token; /** * local address to bind to for listening in source mode or a local socket address for destination mode, * defaults localhost. diff --git a/src/TcpAdapterProxy.cpp b/src/TcpAdapterProxy.cpp index 0faa763..86d1a76 100644 --- a/src/TcpAdapterProxy.cpp +++ b/src/TcpAdapterProxy.cpp @@ -42,6 +42,7 @@ namespace aws { namespace iot { namespace securedtunneling { char const * const PROXY_MODE_QUERY_PARAM = "local-proxy-mode"; char const * const ACCESS_TOKEN_HEADER = "access-token"; + char const * const CLIENT_TOKEN_HEADER = "client-token"; char const * const SOURCE_PROXY_MODE = "source"; char const * const DESTINATION_PROXY_MODE = "destination"; char const * const LOCALHOST_IP = "127.0.0.1"; @@ -53,7 +54,6 @@ namespace aws { namespace iot { namespace securedtunneling { com::amazonaws::iot::securedtunneling::Message_Type_DATA, com::amazonaws::iot::securedtunneling::Message_Type_STREAM_RESET}; - std::string get_region_endpoint(std::string const ®ion, boost::property_tree::ptree const &settings) { boost::optional endpoint_override = settings.get_optional( @@ -721,6 +721,10 @@ namespace aws { namespace iot { namespace securedtunneling { { request.set(boost::beast::http::field::sec_websocket_protocol, GET_SETTING(settings, WEB_SOCKET_SUBPROTOCOL)); request.set(ACCESS_TOKEN_HEADER, tac.adapter_config.access_token.c_str()); + if(!tac.adapter_config.client_token.empty()) + { + request.set(CLIENT_TOKEN_HEADER, tac.adapter_config.client_token.c_str()); + } request.set(boost::beast::http::field::user_agent, user_agent_string); BOOST_LOG_SEV(log, trace) << "Web socket ugprade request(*not entirely final):\n" << get_token_filtered_request(request); }, diff --git a/src/TcpAdapterProxy.h b/src/TcpAdapterProxy.h index 2f57f8a..719498e 100644 --- a/src/TcpAdapterProxy.h +++ b/src/TcpAdapterProxy.h @@ -19,6 +19,7 @@ #include #include #include +#include #include "ProxySettings.h" #include "TcpConnection.h" #include "TcpServer.h" diff --git a/src/main.cpp b/src/main.cpp index f60e9dd..491614e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,6 +20,10 @@ #include #include #include +#include + +#include +#include #include "ProxySettings.h" #include "TcpAdapterProxy.h" @@ -47,7 +51,8 @@ using aws::iot::securedtunneling::proxy_mode; using aws::iot::securedtunneling::get_region_endpoint; using aws::iot::securedtunneling::settings::apply_region_overrides; -char const * const TOKEN_ENV_VARIABLE = "AWSIOT_TUNNEL_ACCESS_TOKEN"; +char const * const ACCESS_TOKEN_ENV_VARIABLE = "AWSIOT_TUNNEL_ACCESS_TOKEN"; +char const * const CLIENT_TOKEN_ENV_VARIABLE = "AWSIOT_TUNNEL_CLIENT_TOKEN"; char const * const ENDPOINT_ENV_VARIABLE = "AWSIOT_TUNNEL_ENDPOINT"; char const * const REGION_ENV_VARIABLE = "AWSIOT_TUNNEL_REGION"; char const * const WEB_PROXY_ENV_VARIABLE = "HTTPS_PROXY"; @@ -143,6 +148,7 @@ bool process_cli(int argc, char ** argv, LocalproxyConfig &cfg, ptree &settings, cliargs_desc.add_options() ("help,h", "Show help message") ("access-token,t", value()->required(), "Client access token") + ("client-token,i", value(), "Optional Client Token") ("proxy-endpoint,e", value(), "Endpoint of proxy server with port (if not default 443). Example: data.tunneling.iot.us-east-1.amazonaws.com:443") ("region,r", value(), "Endpoint region where tunnel exists. Mutually exclusive flag with --proxy-endpoint") ("source-listen-port,s", value(), "Sets the mappings between source listening ports and service identifier. Example: SSH1=5555 or 5555") @@ -186,8 +192,10 @@ bool process_cli(int argc, char ** argv, LocalproxyConfig &cfg, ptree &settings, store(parse_environment(cliargs_desc, [](std::string name) -> std::string { - if (name == TOKEN_ENV_VARIABLE) + if (name == ACCESS_TOKEN_ENV_VARIABLE) return "access-token"; + if (name == CLIENT_TOKEN_ENV_VARIABLE) + return "client-token"; if (name == ENDPOINT_ENV_VARIABLE) return "proxy-endpoint"; if (name == REGION_ENV_VARIABLE) @@ -212,10 +220,15 @@ bool process_cli(int argc, char ** argv, LocalproxyConfig &cfg, ptree &settings, notify(vm); if (token_cli_warning) { - BOOST_LOG_TRIVIAL(warning) << "Found access token supplied via CLI arg. Consider using environment variable " << TOKEN_ENV_VARIABLE << " instead"; + BOOST_LOG_TRIVIAL(warning) << "Found access token supplied via CLI arg. Consider using environment variable " << ACCESS_TOKEN_ENV_VARIABLE << " instead"; } cfg.access_token = vm["access-token"].as(); + if (vm.count("client-token") != 0) + { + cfg.client_token = vm["client-token"].as(); + } + string proxy_endpoint = vm.count("proxy-endpoint") == 1 ? vm["proxy-endpoint"].as() : get_region_endpoint(vm["region"].as(), settings); diff --git a/test/AdapterTests.cpp b/test/AdapterTests.cpp index 8319b2d..2c86d56 100644 --- a/test/AdapterTests.cpp +++ b/test/AdapterTests.cpp @@ -278,6 +278,106 @@ TEST_CASE( "Test source mode", "[source]") { tcp_adapter_thread.join(); } +TEST_CASE( "Test source mode with client token", "[source]") { + using namespace com::amazonaws::iot::securedtunneling; + /** + * Test case set up + * 1. Create tcp socket to acts as destination app. + * 2. Create web socket server to act as secure tunneling service (cloud side). + * 3. Configure adapter config used for the local proxy. + */ + boost::asio::io_context io_ctx{}; + tcp::socket client_socket{ io_ctx }; + + boost::system::error_code ec; + ptree settings; + apply_test_settings(settings); + TestWebsocketServer ws_server(LOCALHOST, settings); + tcp::endpoint ws_address{ws_server.get_endpoint()}; + std::cout << "Test server is listening on address: " << ws_address.address() << " and port: " << ws_address.port() << endl; + + LocalproxyConfig adapter_cfg; + apply_test_config(adapter_cfg, ws_address); + adapter_cfg.mode = proxy_mode::SOURCE; + adapter_cfg.bind_address = LOCALHOST; + adapter_cfg.access_token = "foobar_token"; + adapter_cfg.client_token = "foobar-client-token"; + const std::string service_id= "ssh1"; + uint16_t adapter_chosen_port = get_available_port(io_ctx); + adapter_cfg.serviceId_to_endpoint_map[service_id] = boost::lexical_cast(adapter_chosen_port); + + tcp_adapter_proxy proxy{ settings, adapter_cfg }; + + //start web socket server thread and tcp adapter threads + thread ws_server_thread{[&ws_server]() { ws_server.run(); } }; + thread tcp_adapter_thread{[&proxy]() { proxy.run_proxy(); } }; + + // Verify web socket handshake request from local proxy + this_thread::sleep_for(chrono::milliseconds(IO_PAUSE_MS)); + CHECK( ws_server.get_handshake_request().method() == boost::beast::http::verb::get ); + CHECK( ws_server.get_handshake_request().target() == "/tunnel?local-proxy-mode=source" ); + CHECK( ws_server.get_handshake_request().base()["sec-websocket-protocol"] == "aws.iot.securetunneling-2.0" ); + CHECK( ws_server.get_handshake_request().base()["access-token"] == adapter_cfg.access_token ); + CHECK( ws_server.get_handshake_request().base()["client-token"] == adapter_cfg.client_token ); + + // Simulate cloud side sends control message Message_Type_SERVICE_IDS + message ws_server_message{}; + ws_server_message.set_type(Message_Type_SERVICE_IDS); + ws_server_message.add_availableserviceids(service_id); + ws_server_message.set_ignorable(false); + ws_server_message.clear_payload(); + + ws_server.deliver_message(ws_server_message); + this_thread::sleep_for(chrono::milliseconds(IO_PAUSE_MS)); + + // Simulate source app connects to source local proxy + client_socket.connect( tcp::endpoint{boost::asio::ip::make_address(adapter_cfg.bind_address.get()), adapter_chosen_port} ); + + uint8_t read_buffer[READ_BUFFER_SIZE]; + + // Simulate sending data messages from source app + for(int i = 0; i < 5; ++i) + { + string const test_string = (boost::format("test message: %1%") % i).str(); + client_socket.send(boost::asio::buffer(test_string)); + client_socket.read_some(boost::asio::buffer(reinterpret_cast(read_buffer), READ_BUFFER_SIZE)); + CHECK( string(reinterpret_cast(read_buffer)) == test_string ); + } + + // Verify local proxy sends Message_Type_STREAM_RESET + ws_server.expect_next_message( + [](message const&msg) + { + return (msg.type() == com::amazonaws::iot::securedtunneling::Message_Type_STREAM_RESET) && msg.streamid() == 1; + }); + client_socket.close(); + + this_thread::sleep_for(chrono::milliseconds(IO_PAUSE_MS)); + + // Simulate source app connects to source local proxy + client_socket.connect( tcp::endpoint{boost::asio::ip::make_address(adapter_cfg.bind_address.get()), adapter_chosen_port} ); + + // Simulate sending data messages from source app + for(int i = 0; i < 5; ++i) + { + string const test_string = (boost::format("test message: %1%") % i).str(); + client_socket.send(boost::asio::buffer(test_string)); + client_socket.read_some(boost::asio::buffer(reinterpret_cast(read_buffer), READ_BUFFER_SIZE)); + CHECK( string(reinterpret_cast(read_buffer)) == test_string ); + } + + //instruct websocket to close on client + ws_server.close_client("test_closure", boost::beast::websocket::internal_error); + //attempt a read on the client which should now see the socket EOF (peer closed) caused by adapter + client_socket.read_some(boost::asio::buffer(reinterpret_cast(read_buffer), READ_BUFFER_SIZE), ec); + CHECK( ec.value() == BOOST_EC_SOCKET_CLOSED ); + + client_socket.close(); + + ws_server_thread.join(); + tcp_adapter_thread.join(); +} + TEST_CASE( "Test destination mode", "[destination]") { using namespace com::amazonaws::iot::securedtunneling;