From f80b23750bea597a8bfc310c64a1a4699719ad6c Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 2 Feb 2024 23:01:33 -0600 Subject: [PATCH] Implement RTSP encryption support RTSP encryption is mandatory for client that report core version 1 or later. --- src/nvhttp.cpp | 26 +++- src/rtsp.cpp | 334 +++++++++++++++++++++++++++++++++++++++++-------- src/rtsp.h | 4 + 3 files changed, 310 insertions(+), 54 deletions(-) diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 285df69d..ba7d068c 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -295,6 +295,16 @@ namespace nvhttp { launch_session->gcmap = util::from_view(get_arg(args, "gcmap", "0")); launch_session->enable_hdr = util::from_view(get_arg(args, "hdrMode", "0")); + // Encrypted RTSP is enabled with client reported corever >= 1 + auto corever = util::from_view(get_arg(args, "corever", "0")); + if (corever >= 1) { + launch_session->rtsp_cipher = crypto::cipher::gcm_t { + launch_session->gcm_key, false + }; + launch_session->rtsp_iv_counter = 0; + } + launch_session->rtsp_url_scheme = launch_session->rtsp_cipher ? "rtspenc://"s : "rtsp://"s; + // Generate the unique identifiers for this connection that we will send later during RTSP handshake unsigned char raw_payload[8]; RAND_bytes(raw_payload, sizeof(raw_payload)); @@ -821,11 +831,13 @@ namespace nvhttp { } } - rtsp_stream::launch_session_raise(launch_session); - tree.put("root..status_code", 200); - tree.put("root.sessionUrl0", "rtsp://"s + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); + tree.put("root.sessionUrl0", launch_session->rtsp_url_scheme + + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); tree.put("root.gamesession", 1); + + rtsp_stream::launch_session_raise(launch_session); } void @@ -892,11 +904,15 @@ namespace nvhttp { } } - rtsp_stream::launch_session_raise(make_launch_session(host_audio, args)); + auto launch_session = make_launch_session(host_audio, args); tree.put("root..status_code", 200); - tree.put("root.sessionUrl0", "rtsp://"s + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); + tree.put("root.sessionUrl0", launch_session->rtsp_url_scheme + + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); tree.put("root.resume", 1); + + rtsp_stream::launch_session_raise(launch_session); } void diff --git a/src/rtsp.cpp b/src/rtsp.cpp index 5fa0e024..ba3842c5 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -41,6 +41,40 @@ namespace rtsp_stream { delete msg; } +#pragma pack(push, 1) + + struct encrypted_rtsp_header_t { + // We set the MSB in encrypted RTSP messages to allow format-agnostic + // parsing code to be able to tell encrypted from plaintext messages. + static constexpr std::uint32_t ENCRYPTED_MESSAGE_TYPE_BIT = 0x80000000; + + uint8_t * + payload() { + return (uint8_t *) (this + 1); + } + + std::uint32_t + payload_length() { + return util::endian::big(typeAndLength) & ~ENCRYPTED_MESSAGE_TYPE_BIT; + } + + bool + is_encrypted() { + return !!(util::endian::big(typeAndLength) & ENCRYPTED_MESSAGE_TYPE_BIT); + } + + // This field is the length of the payload + ENCRYPTED_MESSAGE_TYPE_BIT in big-endian + std::uint32_t typeAndLength; + + // This field is the number used to initialize the bottom 4 bytes of the AES IV in big-endian + std::uint32_t sequenceNumber; + + // This field is the AES GCM authentication tag + std::uint8_t tag[16]; + }; + +#pragma pack(pop) + class rtsp_server_t; using msg_t = util::safe_ptr; @@ -58,61 +92,207 @@ namespace rtsp_stream { socket_t(boost::asio::io_service &ios, std::function &&handle_data_fn): handle_data_fn { std::move(handle_data_fn) }, sock { ios } {} + /** + * @brief Queues an asynchronous read to begin the next message. + */ void read() { - if (begin == std::end(msg_buf)) { + if (begin == std::end(msg_buf) || (session->rtsp_cipher && begin + sizeof(encrypted_rtsp_header_t) >= std::end(msg_buf))) { BOOST_LOG(error) << "RTSP: read(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); respond(sock, *session, nullptr, 400, "BAD REQUEST", 0, {}); - sock.close(); + boost::system::error_code ec; + sock.close(ec); return; } - sock.async_read_some( - boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)), - boost::bind( - &socket_t::handle_read, shared_from_this(), - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred)); - } - - void - read_payload() { - if (begin == std::end(msg_buf)) { - BOOST_LOG(error) << "RTSP: read_payload(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); - - respond(sock, *session, nullptr, 400, "BAD REQUEST", 0, {}); - - sock.close(); - - return; + if (session->rtsp_cipher) { + // For encrypted RTSP, we will read the the entire header first + boost::asio::async_read(sock, + boost::asio::buffer(begin, sizeof(encrypted_rtsp_header_t)), + boost::bind( + &socket_t::handle_read_encrypted_header, shared_from_this(), + boost::asio::placeholders::error, + boost::asio::placeholders::bytes_transferred)); + } + else { + sock.async_read_some( + boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)), + boost::bind( + &socket_t::handle_read_plaintext, shared_from_this(), + boost::asio::placeholders::error, + boost::asio::placeholders::bytes_transferred)); } - - sock.async_read_some( - boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)), - boost::bind( - &socket_t::handle_payload, shared_from_this(), - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred)); } + /** + * @brief Handles the initial read of the header of an encrypted message. + * @param socket The socket the message was received on. + * @param ec The error code of the read operation. + * @param bytes The number of bytes read. + */ static void - handle_payload(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { - BOOST_LOG(debug) << "handle_payload(): Handle read of size: "sv << bytes << " bytes"sv; + handle_read_encrypted_header(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + BOOST_LOG(debug) << "handle_read_encrypted_header(): Handle read of size: "sv << bytes << " bytes"sv; auto sock_close = util::fail_guard([&socket]() { boost::system::error_code ec; socket->sock.close(ec); if (ec) { - BOOST_LOG(error) << "RTSP: handle_payload(): Couldn't close tcp socket: "sv << ec.message(); + BOOST_LOG(error) << "RTSP: handle_read_encrypted_header(): Couldn't close tcp socket: "sv << ec.message(); + } + }); + + if (ec || bytes < sizeof(encrypted_rtsp_header_t)) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted_header(): Couldn't read from tcp socket: "sv << ec.message(); + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + auto header = (encrypted_rtsp_header_t *) socket->begin; + if (!header->is_encrypted()) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted_header(): Rejecting unencrypted RTSP message"sv; + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + auto payload_length = header->payload_length(); + + // Check if we have enough space to read this message + if (socket->begin + sizeof(*header) + payload_length >= std::end(socket->msg_buf)) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted_header(): Exceeded maximum rtsp packet size: "sv << socket->msg_buf.size(); + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + sock_close.disable(); + + // Read the remainder of the header and full encrypted payload + boost::asio::async_read(socket->sock, + boost::asio::buffer(socket->begin + bytes, payload_length), + boost::bind( + &socket_t::handle_read_encrypted_message, socket->shared_from_this(), + boost::asio::placeholders::error, + boost::asio::placeholders::bytes_transferred)); + } + + /** + * @brief Handles the final read of the content of an encrypted message. + * @param socket The socket the message was received on. + * @param ec The error code of the read operation. + * @param bytes The number of bytes read. + */ + static void + handle_read_encrypted_message(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + BOOST_LOG(debug) << "handle_read_encrypted(): Handle read of size: "sv << bytes << " bytes"sv; + + auto sock_close = util::fail_guard([&socket]() { + boost::system::error_code ec; + socket->sock.close(ec); + + if (ec) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted_message(): Couldn't close tcp socket: "sv << ec.message(); + } + }); + + auto header = (encrypted_rtsp_header_t *) socket->begin; + auto payload_length = header->payload_length(); + auto seq = util::endian::big(header->sequenceNumber); + + if (ec || bytes < payload_length) { + BOOST_LOG(error) << "RTSP: handle_read_encrypted(): Couldn't read from tcp socket: "sv << ec.message(); + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + // We use the deterministic IV construction algorithm specified in NIST SP 800-38D + // Section 8.2.1. The sequence number is our "invocation" field and the 'RC' in the + // high bytes is the "fixed" field. Because each client provides their own unique + // key, our values in the fixed field need only uniquely identify each independent + // use of the client's key with AES-GCM in our code. + // + // The sequence number is 32 bits long which allows for 2^32 RTSP messages to be + // received from each client before the IV repeats. + crypto::aes_t iv(12); + std::copy_n((uint8_t *) &seq, sizeof(seq), std::begin(iv)); + iv[10] = 'C'; // Client originated + iv[11] = 'R'; // RTSP + + std::vector plaintext; + if (socket->session->rtsp_cipher->decrypt(std::string_view { (const char *) header->tag, sizeof(header->tag) + bytes }, plaintext, &iv)) { + BOOST_LOG(error) << "Failed to verify RTSP message tag"sv; + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + msg_t req { new msg_t::element_type {} }; + if (auto status = parseRtspMessage(req.get(), (char *) plaintext.data(), plaintext.size())) { + BOOST_LOG(error) << "Malformed RTSP message: ["sv << status << ']'; + + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); + return; + } + + sock_close.disable(); + + print_msg(req.get()); + + socket->handle_data(std::move(req)); + } + + /** + * @brief Queues an asynchronous read of the payload portion of a plaintext message. + */ + void + read_plaintext_payload() { + if (begin == std::end(msg_buf)) { + BOOST_LOG(error) << "RTSP: read_plaintext_payload(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); + + respond(sock, *session, nullptr, 400, "BAD REQUEST", 0, {}); + + boost::system::error_code ec; + sock.close(ec); + + return; + } + + sock.async_read_some( + boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)), + boost::bind( + &socket_t::handle_plaintext_payload, shared_from_this(), + boost::asio::placeholders::error, + boost::asio::placeholders::bytes_transferred)); + } + + /** + * @brief Handles the read of the payload portion of a plaintext message. + * @param socket The socket the message was received on. + * @param ec The error code of the read operation. + * @param bytes The number of bytes read. + */ + static void + handle_plaintext_payload(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + BOOST_LOG(debug) << "handle_plaintext_payload(): Handle read of size: "sv << bytes << " bytes"sv; + + auto sock_close = util::fail_guard([&socket]() { + boost::system::error_code ec; + socket->sock.close(ec); + + if (ec) { + BOOST_LOG(error) << "RTSP: handle_plaintext_payload(): Couldn't close tcp socket: "sv << ec.message(); } }); if (ec) { - BOOST_LOG(error) << "RTSP: handle_payload(): Couldn't read from tcp socket: "sv << ec.message(); + BOOST_LOG(error) << "RTSP: handle_plaintext_payload(): Couldn't read from tcp socket: "sv << ec.message(); return; } @@ -122,14 +302,14 @@ namespace rtsp_stream { if (auto status = parseRtspMessage(req.get(), socket->msg_buf.data(), (std::size_t)(end - socket->msg_buf.data()))) { BOOST_LOG(error) << "Malformed RTSP message: ["sv << status << ']'; - respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", req->sequenceNumber, {}); + respond(socket->sock, *socket->session, nullptr, 400, "BAD REQUEST", 0, {}); return; } sock_close.disable(); auto fg = util::fail_guard([&socket]() { - socket->read_payload(); + socket->read_plaintext_payload(); }); auto content_length = 0; @@ -161,18 +341,24 @@ namespace rtsp_stream { socket->begin = end; } + /** + * @brief Handles the read of the header portion of a plaintext message. + * @param socket The socket the message was received on. + * @param ec The error code of the read operation. + * @param bytes The number of bytes read. + */ static void - handle_read(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { - BOOST_LOG(debug) << "handle_read(): Handle read of size: "sv << bytes << " bytes"sv; + handle_read_plaintext(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + BOOST_LOG(debug) << "handle_read_plaintext(): Handle read of size: "sv << bytes << " bytes"sv; if (ec) { - BOOST_LOG(error) << "RTSP: handle_read(): Couldn't read from tcp socket: "sv << ec.message(); + BOOST_LOG(error) << "RTSP: handle_read_plaintext(): Couldn't read from tcp socket: "sv << ec.message(); boost::system::error_code ec; socket->sock.close(ec); if (ec) { - BOOST_LOG(error) << "RTSP: handle_read(): Couldn't close tcp socket: "sv << ec.message(); + BOOST_LOG(error) << "RTSP: handle_read_plaintext(): Couldn't close tcp socket: "sv << ec.message(); } return; @@ -201,7 +387,7 @@ namespace rtsp_stream { buf_size = end - socket->begin; fg.disable(); - handle_payload(socket, ec, buf_size); + handle_plaintext_payload(socket, ec, buf_size); } void @@ -280,7 +466,8 @@ namespace rtsp_stream { cmd_not_found(sock, session, std::move(req)); } - sock.shutdown(boost::asio::socket_base::shutdown_type::shutdown_both); + boost::system::error_code ec; + sock.shutdown(boost::asio::socket_base::shutdown_type::shutdown_both, ec); } void @@ -293,18 +480,27 @@ namespace rtsp_stream { return; } - auto launch_session { launch_event.view() }; + auto socket = std::move(next_socket); + + auto launch_session { launch_event.view(0s) }; if (launch_session) { // Associate the current RTSP session with this socket and start reading - auto socket = std::move(next_socket); socket->session = launch_session; socket->read(); + } + else { + // This can happen due to normal things like port scanning, so let's not make these visible by default + BOOST_LOG(debug) << "No pending session for incoming RTSP connection"sv; - next_socket = std::make_shared(ios, [this](tcp::socket &sock, launch_session_t &session, msg_t &&msg) { - handle_msg(sock, session, std::move(msg)); - }); + // If there is no session pending, close the connection immediately + boost::system::error_code ec; + socket->sock.close(ec); } + // Queue another asynchronous accept for the next incoming connection + next_socket = std::make_shared(ios, [this](tcp::socket &sock, launch_session_t &session, msg_t &&msg) { + handle_msg(sock, session, std::move(msg)); + }); acceptor.async_accept(next_socket->sock, [this](const auto &ec) { handle_accept(ec); }); @@ -343,7 +539,7 @@ namespace rtsp_stream { session_clear(uint32_t launch_session_id) { // We currently only support a single pending RTSP session, // so the ID should always match the one for that session. - auto launch_session = launch_event.view(); + auto launch_session = launch_event.view(0s); if (launch_session) { if (launch_session->id != launch_session_id) { BOOST_LOG(error) << "Attempted to clear unexpected session: "sv << launch_session_id << " vs "sv << launch_session->id; @@ -498,13 +694,53 @@ namespace rtsp_stream { << std::string_view { payload.first, (std::size_t) payload.second } << std::endl << "---End Response---"sv << std::endl; - std::string_view tmp_resp { raw_resp.get(), (size_t) serialized_len }; + // Encrypt the RTSP message if encryption is enabled + if (session.rtsp_cipher) { + // We use the deterministic IV construction algorithm specified in NIST SP 800-38D + // Section 8.2.1. The sequence number is our "invocation" field and the 'RH' in the + // high bytes is the "fixed" field. Because each client provides their own unique + // key, our values in the fixed field need only uniquely identify each independent + // use of the client's key with AES-GCM in our code. + // + // The sequence number is 32 bits long which allows for 2^32 RTSP messages to be + // sent to each client before the IV repeats. + crypto::aes_t iv(12); + session.rtsp_iv_counter++; + std::copy_n((uint8_t *) &session.rtsp_iv_counter, sizeof(session.rtsp_iv_counter), std::begin(iv)); + iv[10] = 'H'; // Host originated + iv[11] = 'R'; // RTSP - if (send(sock, tmp_resp)) { - return; + // Allocate the message with an empty header and reserved space for the payload + auto payload_length = serialized_len + payload.second; + std::vector message(sizeof(encrypted_rtsp_header_t)); + message.reserve(message.size() + payload_length); + + // Copy the complete plaintext into the message + std::copy_n(raw_resp.get(), serialized_len, std::back_inserter(message)); + std::copy_n(payload.first, payload.second, std::back_inserter(message)); + + // Initialize the message header + auto header = (encrypted_rtsp_header_t *) message.data(); + header->typeAndLength = util::endian::big(encrypted_rtsp_header_t::ENCRYPTED_MESSAGE_TYPE_BIT + payload_length); + header->sequenceNumber = util::endian::big(session.rtsp_iv_counter); + + // Encrypt the RTSP message in place + session.rtsp_cipher->encrypt(std::string_view { (const char *) header->payload(), (std::size_t) payload_length }, header->tag, &iv); + + // Send the full encrypted message + send(sock, std::string_view { (char *) message.data(), message.size() }); } + else { + std::string_view tmp_resp { raw_resp.get(), (size_t) serialized_len }; - send(sock, std::string_view { payload.first, (std::size_t) payload.second }); + // Send the plaintext RTSP message header + if (send(sock, tmp_resp)) { + return; + } + + // Send the plaintext RTSP message payload (if present) + send(sock, std::string_view { payload.first, (std::size_t) payload.second }); + } } void diff --git a/src/rtsp.h b/src/rtsp.h index 01674059..20bb8453 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -31,6 +31,10 @@ namespace rtsp_stream { int surround_info; bool enable_hdr; bool enable_sops; + + std::optional rtsp_cipher; + std::string rtsp_url_scheme; + uint32_t rtsp_iv_counter; }; void