mirror of
https://github.com/LizardByte/Sunshine.git
synced 2025-01-27 03:35:36 +00:00
Merge commit from fork
Some checks failed
CI / GitHub Env Debug (push) Has been cancelled
CI / Setup Release (push) Has been cancelled
CI / Setup Flatpak Matrix (push) Has been cancelled
CI Docker / Check Dockerfiles (push) Has been cancelled
CodeQL / Get language matrix (push) Has been cancelled
localize / Update Localization (push) Has been cancelled
Build GH-Pages / prep (push) Has been cancelled
CI / Linux Flatpak (push) Has been cancelled
CI / Linux ${{ matrix.type }} (--appimage-build, 22.04, AppImage) (push) Has been cancelled
CI / Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) (macos, 13) (push) Has been cancelled
CI / Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) (macos, 14) (push) Has been cancelled
CI / Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) (ubuntu, latest) (push) Has been cancelled
CI / Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) (ubuntu, latest, true) (push) Has been cancelled
CI / Windows (push) Has been cancelled
CI Docker / Setup Release (push) Has been cancelled
CI Docker / Docker${{ matrix.tag }} (push) Has been cancelled
CodeQL / Analyze (${{ matrix.name }}) (push) Has been cancelled
Build GH-Pages / call-jekyll-build (push) Has been cancelled
Some checks failed
CI / GitHub Env Debug (push) Has been cancelled
CI / Setup Release (push) Has been cancelled
CI / Setup Flatpak Matrix (push) Has been cancelled
CI Docker / Check Dockerfiles (push) Has been cancelled
CodeQL / Get language matrix (push) Has been cancelled
localize / Update Localization (push) Has been cancelled
Build GH-Pages / prep (push) Has been cancelled
CI / Linux Flatpak (push) Has been cancelled
CI / Linux ${{ matrix.type }} (--appimage-build, 22.04, AppImage) (push) Has been cancelled
CI / Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) (macos, 13) (push) Has been cancelled
CI / Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) (macos, 14) (push) Has been cancelled
CI / Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) (ubuntu, latest) (push) Has been cancelled
CI / Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) (ubuntu, latest, true) (push) Has been cancelled
CI / Windows (push) Has been cancelled
CI Docker / Setup Release (push) Has been cancelled
CI Docker / Docker${{ matrix.tag }} (push) Has been cancelled
CodeQL / Analyze (${{ matrix.name }}) (push) Has been cancelled
Build GH-Pages / call-jekyll-build (push) Has been cancelled
Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Co-authored-by: Cameron Gutman <2695644+cgutman@users.noreply.github.com>
This commit is contained in:
parent
80fa04c330
commit
89f097ae65
188
src/nvhttp.cpp
188
src/nvhttp.cpp
@ -11,7 +11,6 @@
|
||||
|
||||
// lib includes
|
||||
#include <Simple-Web-Server/server_http.hpp>
|
||||
#include <Simple-Web-Server/server_https.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <boost/asio/ssl/context_base.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
@ -21,7 +20,6 @@
|
||||
|
||||
// local includes
|
||||
#include "config.h"
|
||||
#include "crypto.h"
|
||||
#include "display_device.h"
|
||||
#include "file_handler.h"
|
||||
#include "globals.h"
|
||||
@ -45,18 +43,6 @@ namespace nvhttp {
|
||||
|
||||
crypto::cert_chain_t cert_chain;
|
||||
|
||||
class SunshineHTTPS: public SimpleWeb::HTTPS {
|
||||
public:
|
||||
SunshineHTTPS(boost::asio::io_context &io_context, boost::asio::ssl::context &ctx):
|
||||
SimpleWeb::HTTPS(io_context, ctx) {}
|
||||
|
||||
virtual ~SunshineHTTPS() {
|
||||
// Gracefully shutdown the TLS connection
|
||||
SimpleWeb::error_code ec;
|
||||
shutdown(ec);
|
||||
}
|
||||
};
|
||||
|
||||
class SunshineHTTPSServer: public SimpleWeb::ServerBase<SunshineHTTPS> {
|
||||
public:
|
||||
SunshineHTTPSServer(const std::string &certification_file, const std::string &private_key_file):
|
||||
@ -146,28 +132,6 @@ namespace nvhttp {
|
||||
std::vector<named_cert_t> named_devices;
|
||||
};
|
||||
|
||||
struct pair_session_t {
|
||||
struct {
|
||||
std::string uniqueID;
|
||||
std::string cert;
|
||||
std::string name;
|
||||
} client;
|
||||
|
||||
std::unique_ptr<crypto::aes_t> cipher_key;
|
||||
std::vector<uint8_t> clienthash;
|
||||
|
||||
std::string serversecret;
|
||||
std::string serverchallenge;
|
||||
|
||||
struct {
|
||||
util::Either<
|
||||
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>,
|
||||
std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Response>>
|
||||
response;
|
||||
std::string salt;
|
||||
} async_insert_pin;
|
||||
};
|
||||
|
||||
// uniqueID, session
|
||||
std::unordered_map<std::string, pair_session_t> map_id_sess;
|
||||
client_t client_root;
|
||||
@ -367,12 +331,29 @@ namespace nvhttp {
|
||||
return launch_session;
|
||||
}
|
||||
|
||||
void
|
||||
remove_session(const pair_session_t &sess) {
|
||||
map_id_sess.erase(sess.client.uniqueID);
|
||||
}
|
||||
|
||||
void
|
||||
fail_pair(pair_session_t &sess, pt::ptree &tree, const std::string status_msg) {
|
||||
tree.put("root.paired", 0);
|
||||
tree.put("root.<xmlattr>.status_code", 400);
|
||||
tree.put("root.<xmlattr>.status_message", status_msg);
|
||||
remove_session(sess); // Security measure, delete the session when something went wrong and force a re-pair
|
||||
}
|
||||
|
||||
void
|
||||
getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin) {
|
||||
if (sess.last_phase != PAIR_PHASE::NONE) {
|
||||
fail_pair(sess, tree, "Out of order call to getservercert");
|
||||
return;
|
||||
}
|
||||
sess.last_phase = PAIR_PHASE::GETSERVERCERT;
|
||||
|
||||
if (sess.async_insert_pin.salt.size() < 32) {
|
||||
tree.put("root.paired", 0);
|
||||
tree.put("root.<xmlattr>.status_code", 400);
|
||||
tree.put("root.<xmlattr>.status_message", "Salt too short");
|
||||
fail_pair(sess, tree, "Salt too short");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -389,30 +370,17 @@ namespace nvhttp {
|
||||
}
|
||||
|
||||
void
|
||||
serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &args) {
|
||||
auto encrypted_response = util::from_hex_vec(get_arg(args, "serverchallengeresp"), true);
|
||||
|
||||
std::vector<uint8_t> decrypted;
|
||||
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
|
||||
|
||||
cipher.decrypt(encrypted_response, decrypted);
|
||||
|
||||
sess.clienthash = std::move(decrypted);
|
||||
|
||||
auto serversecret = sess.serversecret;
|
||||
auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret);
|
||||
|
||||
serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign));
|
||||
|
||||
tree.put("root.pairingsecret", util::hex_vec(serversecret, true));
|
||||
tree.put("root.paired", 1);
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
}
|
||||
|
||||
void
|
||||
clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args) {
|
||||
auto challenge = util::from_hex_vec(get_arg(args, "clientchallenge"), true);
|
||||
clientchallenge(pair_session_t &sess, pt::ptree &tree, const std::string &challenge) {
|
||||
if (sess.last_phase != PAIR_PHASE::GETSERVERCERT) {
|
||||
fail_pair(sess, tree, "Out of order call to clientchallenge");
|
||||
return;
|
||||
}
|
||||
sess.last_phase = PAIR_PHASE::CLIENTCHALLENGE;
|
||||
|
||||
if (!sess.cipher_key) {
|
||||
fail_pair(sess, tree, "Cipher key not set");
|
||||
return;
|
||||
}
|
||||
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
|
||||
|
||||
std::vector<uint8_t> decrypted;
|
||||
@ -446,21 +414,58 @@ namespace nvhttp {
|
||||
}
|
||||
|
||||
void
|
||||
clientpairingsecret(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, pair_session_t &sess, pt::ptree &tree, const args_t &args) {
|
||||
auto &client = sess.client;
|
||||
serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const std::string &encrypted_response) {
|
||||
if (sess.last_phase != PAIR_PHASE::CLIENTCHALLENGE) {
|
||||
fail_pair(sess, tree, "Out of order call to serverchallengeresp");
|
||||
return;
|
||||
}
|
||||
sess.last_phase = PAIR_PHASE::SERVERCHALLENGERESP;
|
||||
|
||||
auto pairingsecret = util::from_hex_vec(get_arg(args, "clientpairingsecret"), true);
|
||||
if (pairingsecret.size() <= 16) {
|
||||
tree.put("root.paired", 0);
|
||||
tree.put("root.<xmlattr>.status_code", 400);
|
||||
tree.put("root.<xmlattr>.status_message", "Clientpairingsecret too short");
|
||||
if (!sess.cipher_key || sess.serversecret.empty()) {
|
||||
fail_pair(sess, tree, "Cipher key or serversecret not set");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string_view secret { pairingsecret.data(), 16 };
|
||||
std::string_view sign { pairingsecret.data() + secret.size(), pairingsecret.size() - secret.size() };
|
||||
std::vector<uint8_t> decrypted;
|
||||
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
|
||||
|
||||
cipher.decrypt(encrypted_response, decrypted);
|
||||
|
||||
sess.clienthash = std::move(decrypted);
|
||||
|
||||
auto serversecret = sess.serversecret;
|
||||
auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret);
|
||||
|
||||
serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign));
|
||||
|
||||
tree.put("root.pairingsecret", util::hex_vec(serversecret, true));
|
||||
tree.put("root.paired", 1);
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
}
|
||||
|
||||
void
|
||||
clientpairingsecret(pair_session_t &sess, std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, pt::ptree &tree, const std::string &client_pairing_secret) {
|
||||
if (sess.last_phase != PAIR_PHASE::SERVERCHALLENGERESP) {
|
||||
fail_pair(sess, tree, "Out of order call to clientpairingsecret");
|
||||
return;
|
||||
}
|
||||
sess.last_phase = PAIR_PHASE::CLIENTPAIRINGSECRET;
|
||||
|
||||
auto &client = sess.client;
|
||||
|
||||
if (client_pairing_secret.size() <= 16) {
|
||||
fail_pair(sess, tree, "Client pairing secret too short");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string_view secret { client_pairing_secret.data(), 16 };
|
||||
std::string_view sign { client_pairing_secret.data() + secret.size(), client_pairing_secret.size() - secret.size() };
|
||||
|
||||
auto x509 = crypto::x509(client.cert);
|
||||
if (!x509) {
|
||||
fail_pair(sess, tree, "Invalid client certificate");
|
||||
return;
|
||||
}
|
||||
auto x509_sign = crypto::signature(x509);
|
||||
|
||||
std::string data;
|
||||
@ -473,20 +478,20 @@ namespace nvhttp {
|
||||
auto hash = crypto::hash(data);
|
||||
|
||||
// if hash not correct, probably MITM
|
||||
if (!std::memcmp(hash.data(), sess.clienthash.data(), hash.size()) && crypto::verify256(crypto::x509(client.cert), secret, sign)) {
|
||||
bool same_hash = hash.size() == sess.clienthash.size() && std::equal(hash.begin(), hash.end(), sess.clienthash.begin());
|
||||
auto verify = crypto::verify256(crypto::x509(client.cert), secret, sign);
|
||||
if (same_hash && verify) {
|
||||
tree.put("root.paired", 1);
|
||||
add_cert->raise(crypto::x509(client.cert));
|
||||
|
||||
// The client is now successfully paired and will be authorized to connect
|
||||
auto it = map_id_sess.find(client.uniqueID);
|
||||
add_authorized_client(client.name, std::move(client.cert));
|
||||
map_id_sess.erase(it);
|
||||
}
|
||||
else {
|
||||
map_id_sess.erase(client.uniqueID);
|
||||
tree.put("root.paired", 0);
|
||||
}
|
||||
|
||||
remove_session(sess);
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
}
|
||||
|
||||
@ -568,7 +573,6 @@ namespace nvhttp {
|
||||
}
|
||||
|
||||
auto uniqID { get_arg(args, "uniqueid") };
|
||||
auto sess_it = map_id_sess.find(uniqID);
|
||||
|
||||
args_t::const_iterator it;
|
||||
if (it = args.find("phrase"); it != std::end(args)) {
|
||||
@ -603,16 +607,29 @@ namespace nvhttp {
|
||||
else if (it->second == "pairchallenge"sv) {
|
||||
tree.put("root.paired", 1);
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (it = args.find("clientchallenge"); it != std::end(args)) {
|
||||
clientchallenge(sess_it->second, tree, args);
|
||||
|
||||
auto sess_it = map_id_sess.find(uniqID);
|
||||
if (sess_it == std::end(map_id_sess)) {
|
||||
tree.put("root.<xmlattr>.status_code", 400);
|
||||
tree.put("root.<xmlattr>.status_message", "Invalid uniqueid");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (it = args.find("clientchallenge"); it != std::end(args)) {
|
||||
auto challenge = util::from_hex_vec(it->second, true);
|
||||
clientchallenge(sess_it->second, tree, challenge);
|
||||
}
|
||||
else if (it = args.find("serverchallengeresp"); it != std::end(args)) {
|
||||
serverchallengeresp(sess_it->second, tree, args);
|
||||
auto encrypted_response = util::from_hex_vec(it->second, true);
|
||||
serverchallengeresp(sess_it->second, tree, encrypted_response);
|
||||
}
|
||||
else if (it = args.find("clientpairingsecret"); it != std::end(args)) {
|
||||
clientpairingsecret(add_cert, sess_it->second, tree, args);
|
||||
auto pairingsecret = util::from_hex_vec(it->second, true);
|
||||
clientpairingsecret(sess_it->second, add_cert, tree, pairingsecret);
|
||||
}
|
||||
else {
|
||||
tree.put("root.<xmlattr>.status_code", 404);
|
||||
@ -1030,6 +1047,12 @@ namespace nvhttp {
|
||||
response->close_connection_after_response = true;
|
||||
}
|
||||
|
||||
void
|
||||
setup(const std::string &pkey, const std::string &cert) {
|
||||
conf_intern.pkey = pkey;
|
||||
conf_intern.servercert = cert;
|
||||
}
|
||||
|
||||
void
|
||||
start() {
|
||||
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
|
||||
@ -1044,8 +1067,9 @@ namespace nvhttp {
|
||||
load_state();
|
||||
}
|
||||
|
||||
conf_intern.pkey = file_handler::read_file(config::nvhttp.pkey.c_str());
|
||||
conf_intern.servercert = file_handler::read_file(config::nvhttp.cert.c_str());
|
||||
auto pkey = file_handler::read_file(config::nvhttp.pkey.c_str());
|
||||
auto cert = file_handler::read_file(config::nvhttp.cert.c_str());
|
||||
setup(pkey, cert);
|
||||
|
||||
auto add_cert = std::make_shared<safe::queue_t<crypto::x509_t>>(30);
|
||||
|
||||
|
119
src/nvhttp.h
119
src/nvhttp.h
@ -9,9 +9,11 @@
|
||||
#include <string>
|
||||
|
||||
// lib includes
|
||||
#include <Simple-Web-Server/server_https.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
|
||||
// local includes
|
||||
#include "crypto.h"
|
||||
#include "thread_safe.h"
|
||||
|
||||
/**
|
||||
@ -50,6 +52,123 @@ namespace nvhttp {
|
||||
void
|
||||
start();
|
||||
|
||||
/**
|
||||
* @brief Setup the nvhttp server.
|
||||
* @param pkey
|
||||
* @param cert
|
||||
*/
|
||||
void
|
||||
setup(const std::string &pkey, const std::string &cert);
|
||||
|
||||
class SunshineHTTPS: public SimpleWeb::HTTPS {
|
||||
public:
|
||||
SunshineHTTPS(boost::asio::io_context &io_context, boost::asio::ssl::context &ctx):
|
||||
SimpleWeb::HTTPS(io_context, ctx) {}
|
||||
|
||||
virtual ~SunshineHTTPS() {
|
||||
// Gracefully shutdown the TLS connection
|
||||
SimpleWeb::error_code ec;
|
||||
shutdown(ec);
|
||||
}
|
||||
};
|
||||
|
||||
enum class PAIR_PHASE {
|
||||
NONE, ///< Sunshine is not in a pairing phase
|
||||
GETSERVERCERT, ///< Sunshine is in the get server certificate phase
|
||||
CLIENTCHALLENGE, ///< Sunshine is in the client challenge phase
|
||||
SERVERCHALLENGERESP, ///< Sunshine is in the server challenge response phase
|
||||
CLIENTPAIRINGSECRET ///< Sunshine is in the client pairing secret phase
|
||||
};
|
||||
|
||||
struct pair_session_t {
|
||||
struct {
|
||||
std::string uniqueID = {};
|
||||
std::string cert = {};
|
||||
std::string name = {};
|
||||
} client;
|
||||
|
||||
std::unique_ptr<crypto::aes_t> cipher_key = {};
|
||||
std::vector<uint8_t> clienthash = {};
|
||||
|
||||
std::string serversecret = {};
|
||||
std::string serverchallenge = {};
|
||||
|
||||
struct {
|
||||
util::Either<
|
||||
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>,
|
||||
std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Response>>
|
||||
response;
|
||||
std::string salt = {};
|
||||
} async_insert_pin;
|
||||
|
||||
/**
|
||||
* @brief used as a security measure to prevent out of order calls
|
||||
*/
|
||||
PAIR_PHASE last_phase = PAIR_PHASE::NONE;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief removes the temporary pairing session
|
||||
* @param sess
|
||||
*/
|
||||
void
|
||||
remove_session(const pair_session_t &sess);
|
||||
|
||||
/**
|
||||
* @brief Pair, phase 1
|
||||
*
|
||||
* Moonlight will send a salt and client certificate, we'll also need the user provided pin.
|
||||
*
|
||||
* PIN and SALT will be used to derive a shared AES key that needs to be stored
|
||||
* in order to be used to decrypt_symmetric in the next phases.
|
||||
*
|
||||
* At this stage we only have to send back our public certificate.
|
||||
*/
|
||||
void
|
||||
getservercert(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &pin);
|
||||
|
||||
/**
|
||||
* @brief Pair, phase 2
|
||||
*
|
||||
* Using the AES key that we generated in phase 1 we have to decrypt the client challenge,
|
||||
*
|
||||
* We generate a SHA256 hash with the following:
|
||||
* - Decrypted challenge
|
||||
* - Server certificate signature
|
||||
* - Server secret: a randomly generated secret
|
||||
*
|
||||
* The hash + server_challenge will then be AES encrypted and sent as the `challengeresponse` in the returned XML
|
||||
*/
|
||||
void
|
||||
clientchallenge(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &challenge);
|
||||
|
||||
/**
|
||||
* @brief Pair, phase 3
|
||||
*
|
||||
* Moonlight will send back a `serverchallengeresp`: an AES encrypted client hash,
|
||||
* we have to send back the `pairingsecret`:
|
||||
* using our private key we have to sign the certificate_signature + server_secret (generated in phase 2)
|
||||
*/
|
||||
void
|
||||
serverchallengeresp(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &encrypted_response);
|
||||
|
||||
/**
|
||||
* @brief Pair, phase 4 (final)
|
||||
*
|
||||
* We now have to use everything we exchanged before in order to verify and finally pair the clients
|
||||
*
|
||||
* We'll check the client_hash obtained at phase 3, it should contain the following:
|
||||
* - The original server_challenge
|
||||
* - The signature of the X509 client_cert
|
||||
* - The unencrypted client_pairing_secret
|
||||
* We'll check that SHA256(server_challenge + client_public_cert_signature + client_secret) == client_hash
|
||||
*
|
||||
* Then using the client certificate public key we should be able to verify that
|
||||
* the client secret has been signed by Moonlight
|
||||
*/
|
||||
void
|
||||
clientpairingsecret(pair_session_t &sess, std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, boost::property_tree::ptree &tree, const std::string &client_pairing_secret);
|
||||
|
||||
/**
|
||||
* @brief Compare the user supplied pin to the Moonlight pin.
|
||||
* @param pin The user supplied pin.
|
||||
|
@ -45,6 +45,10 @@ file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS
|
||||
set(SUNSHINE_SOURCES
|
||||
${SUNSHINE_TARGET_FILES})
|
||||
|
||||
# copy fixtures to build directory
|
||||
file(COPY ${CMAKE_SOURCE_DIR}/tests/fixtures/unit
|
||||
DESTINATION ${CMAKE_BINARY_DIR}/tests/fixtures)
|
||||
|
||||
# remove main.cpp from the list of sources
|
||||
list(REMOVE_ITEM SUNSHINE_SOURCES ${CMAKE_SOURCE_DIR}/src/main.cpp)
|
||||
|
||||
|
28
tests/fixtures/unit/pairing_test_key.pem
vendored
Normal file
28
tests/fixtures/unit/pairing_test_key.pem
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDLePNlWN06FLlM
|
||||
ujWzIX8UICO7SWfH5DXlafVjpxwi/WCkdO6FxixqRNGu71wMvJXFbDlNR8fqX2xo
|
||||
+eq17J3uFKn+qdjmP3L38bkqxhoJ/nCrXkeGyCTQ+Daug63ZYSJeW2Mmf+LAR5/i
|
||||
/fWYfXpSlbcf5XJQPEWvENpLqWu+NOU50dJXIEVYpUXRx2+x4ZbwkH7tVJm94L+C
|
||||
OUyiJKQPyWgU2aFsyJGwHFfePfSUpfYHqbHZV/ILpY59VJairBwE99bx/mBvMI7a
|
||||
hBmJTSDuDffJcPDhFF5kZa0UkQPrPvhXcQaSRti7v0VonEQj8pTSnGYr9ktWKk92
|
||||
wxDyn9S3AgMBAAECggEAbEhQ14WELg2rUz7hpxPTaiV0fo4hEcrMN+u8sKzVF3Xa
|
||||
QYsNCNoe9urq3/r39LtDxU3D7PGfXYYszmz50Jk8ruAGW8WN7XKkv3i/fxjv8JOc
|
||||
6EYDMKJAnYkKqLLhCQddX/Oof2udg5BacVWPpvhX6a1NSEc2H6cDupfwZEWkVhMi
|
||||
bCC3JcNmjFa8N7ow1/5VQiYVTjpxfV7GY1GRe7vMvBucdQKH3tUG5PYXKXytXw/j
|
||||
KDLaECiYVT89KbApkI0zhy7I5g3LRq0Rs5fmYLCjVebbuAL1W5CJHFJeFOgMKvnO
|
||||
QSl7MfHkTnzTzUqwkwXjgNMGsTosV4UloL9gXVF6GQKBgQD5fI771WETkpaKjWBe
|
||||
6XUVSS98IOAPbTGpb8CIhSjzCuztNAJ+0ey1zklQHonMFbdmcWTkTJoF3ECqAos9
|
||||
vxB4ROg+TdqGDcRrXa7Twtmhv66QvYxttkaK3CqoLX8CCTnjgXBCijo6sCpo6H1T
|
||||
+y55bBDpxZjNFT5BV3+YPBfWQwKBgQDQyNt+saTqJqxGYV7zWQtOqKORRHAjaJpy
|
||||
m5035pky5wORsaxQY8HxbsTIQp9jBSw3SQHLHN/NAXDl2k7VAw/axMc+lj9eW+3z
|
||||
2Hv5LVgj37jnJYEpYwehvtR0B4jZnXLyLwShoBdRPkGlC5fs9+oWjQZoDwMLZfTg
|
||||
eZVOJm6SfQKBgQDfxYcB/kuKIKsCLvhHaSJpKzF6JoqRi6FFlkScrsMh66TCxSmP
|
||||
0n58O0Cqqhlyge/z5LVXyBVGOF2Pn6SAh4UgOr4MVAwyvNp2aprKuTQ2zhSnIjx4
|
||||
k0sGdZ+VJOmMS/YuRwUHya+cwDHp0s3Gq77tja5F38PD/s/OD8sUIqJGvQKBgBfI
|
||||
6ghy4GC0ayfRa+m5GSqq14dzDntaLU4lIDIAGS/NVYDBhunZk3yXq99Mh6/WJQVf
|
||||
Uc77yRsnsN7ekeB+as33YONmZm2vd1oyLV1jpwjfMcdTZHV8jKAGh1l4ikSQRUoF
|
||||
xTdMb5uXxg6xVWtvisFq63HrU+N2iAESmMnAYxRZAoGAVEFJRRjPrSIUTCCKRiTE
|
||||
br+cHqy6S5iYRxGl9riKySBKeU16fqUACIvUqmqlx4Secj3/Hn/VzYEzkxcSPwGi
|
||||
qMgdS0R+tacca7NopUYaaluneKYdS++DNlT/m+KVHqLynQr54z1qBlThg9KGrpmM
|
||||
LGZkXtQpx6sX7v3Kq56PkNk=
|
||||
-----END PRIVATE KEY-----
|
18
tests/fixtures/unit/pairing_test_public.cert
vendored
Normal file
18
tests/fixtures/unit/pairing_test_public.cert
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC6zCCAdOgAwIBAgIBATANBgkqhkiG9w0BAQsFADA5MQswCQYDVQQGEwJJVDEW
|
||||
MBQGA1UECgwNR2FtZXNPbldoYWxlczESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIy
|
||||
MDQwOTA5MTYwNVoXDTQyMDQwNDA5MTYwNVowOTELMAkGA1UEBhMCSVQxFjAUBgNV
|
||||
BAoMDUdhbWVzT25XaGFsZXMxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZI
|
||||
hvcNAQEBBQADggEPADCCAQoCggEBAMt482VY3ToUuUy6NbMhfxQgI7tJZ8fkNeVp
|
||||
9WOnHCL9YKR07oXGLGpE0a7vXAy8lcVsOU1Hx+pfbGj56rXsne4Uqf6p2OY/cvfx
|
||||
uSrGGgn+cKteR4bIJND4Nq6DrdlhIl5bYyZ/4sBHn+L99Zh9elKVtx/lclA8Ra8Q
|
||||
2kupa7405TnR0lcgRVilRdHHb7HhlvCQfu1Umb3gv4I5TKIkpA/JaBTZoWzIkbAc
|
||||
V9499JSl9gepsdlX8guljn1UlqKsHAT31vH+YG8wjtqEGYlNIO4N98lw8OEUXmRl
|
||||
rRSRA+s++FdxBpJG2Lu/RWicRCPylNKcZiv2S1YqT3bDEPKf1LcCAwEAATANBgkq
|
||||
hkiG9w0BAQsFAAOCAQEAqPBqzvDjl89pZMll3Ge8RS7HeDuzgocrhOcT2jnk4ag7
|
||||
/TROZuISjDp6+SnL3gPEt7E2OcFAczTg3l/wbT5PFb6vM96saLm4EP0zmLfK1FnM
|
||||
JDRahKutP9rx6RO5OHqsUB+b4jA4W0L9UnXUoLKbjig501AUix0p52FBxu+HJ90r
|
||||
HlLs3Vo6nj4Z/PZXrzaz8dtQ/KJMpd/g/9xlo6BKAnRk5SI8KLhO4hW6zG0QA56j
|
||||
X4wnh1bwdiidqpcgyuKossLOPxbS786WmsesaAWPnpoY6M8aija+ALwNNuWWmyMg
|
||||
9SVDV76xJzM36Uq7Kg3QJYTlY04WmPIdJHkCtXWf9g==
|
||||
-----END CERTIFICATE-----
|
210
tests/unit/test_http_pairing.cpp
Normal file
210
tests/unit/test_http_pairing.cpp
Normal file
@ -0,0 +1,210 @@
|
||||
/**
|
||||
* @file tests/unit/test_http_pairing.cpp
|
||||
* @brief Test src/nvhttp.cpp HTTP pairing process
|
||||
*/
|
||||
|
||||
#include <src/nvhttp.h>
|
||||
|
||||
#include "../tests_common.h"
|
||||
#include "src/file_handler.h"
|
||||
|
||||
using namespace nvhttp;
|
||||
|
||||
struct pairing_input {
|
||||
std::shared_ptr<pair_session_t> session;
|
||||
/**
|
||||
* Normally server challenge is generated by the server, but for testing purposes
|
||||
* we can override it with a custom value. This way the process is deterministic.
|
||||
*/
|
||||
std::string override_server_challenge;
|
||||
std::string pin;
|
||||
std::string client_challenge;
|
||||
std::string server_challenge_resp;
|
||||
std::string client_pairing_secret;
|
||||
};
|
||||
|
||||
struct pairing_output {
|
||||
bool phase_1_success;
|
||||
bool phase_2_success;
|
||||
bool phase_3_success;
|
||||
bool phase_4_success;
|
||||
};
|
||||
|
||||
const auto PRIVATE_KEY = file_handler::read_file("fixtures/unit/pairing_test_key.pem");
|
||||
const auto PUBLIC_CERT = file_handler::read_file("fixtures/unit/pairing_test_public.cert");
|
||||
|
||||
struct PairingTest: testing::TestWithParam<std::tuple<pairing_input, pairing_output>> {};
|
||||
|
||||
TEST_P(PairingTest, Run) {
|
||||
auto [input, expected] = GetParam();
|
||||
|
||||
boost::property_tree::ptree tree;
|
||||
|
||||
setup(PRIVATE_KEY, PUBLIC_CERT);
|
||||
|
||||
// phase 1
|
||||
getservercert(*input.session, tree, input.pin);
|
||||
ASSERT_EQ(tree.get<int>("root.paired") == 1, expected.phase_1_success);
|
||||
if (!expected.phase_1_success) {
|
||||
return;
|
||||
}
|
||||
|
||||
// phase 2
|
||||
clientchallenge(*input.session, tree, input.client_challenge);
|
||||
ASSERT_EQ(tree.get<int>("root.paired") == 1, expected.phase_2_success);
|
||||
if (!expected.phase_2_success) {
|
||||
return;
|
||||
}
|
||||
|
||||
// phase 3
|
||||
serverchallengeresp(*input.session, tree, input.server_challenge_resp);
|
||||
ASSERT_EQ(tree.get<int>("root.paired") == 1, expected.phase_3_success);
|
||||
if (!expected.phase_3_success) {
|
||||
return;
|
||||
}
|
||||
input.session->serverchallenge = input.override_server_challenge;
|
||||
|
||||
// phase 4
|
||||
auto input_client_cert = input.session->client.cert; // Will be moved
|
||||
auto add_cert = std::make_shared<safe::queue_t<crypto::x509_t>>(30);
|
||||
clientpairingsecret(*input.session, add_cert, tree, input.client_pairing_secret);
|
||||
ASSERT_EQ(tree.get<int>("root.paired") == 1, expected.phase_4_success);
|
||||
|
||||
// Check that we actually added the input client certificate to `add_cert`
|
||||
if (expected.phase_4_success) {
|
||||
ASSERT_EQ(add_cert->peek(), true);
|
||||
auto cert = add_cert->pop();
|
||||
char added_subject_name[256];
|
||||
X509_NAME_oneline(X509_get_subject_name(cert.get()), added_subject_name, sizeof(added_subject_name));
|
||||
|
||||
auto input_cert = crypto::x509(input_client_cert);
|
||||
char original_suject_name[256];
|
||||
X509_NAME_oneline(X509_get_subject_name(input_cert.get()), original_suject_name, sizeof(original_suject_name));
|
||||
|
||||
ASSERT_EQ(std::string(added_subject_name), std::string(original_suject_name));
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
TestWorkingPairing,
|
||||
PairingTest,
|
||||
testing::Values(
|
||||
std::make_tuple(
|
||||
pairing_input {
|
||||
.session = std::make_shared<pair_session_t>(
|
||||
pair_session_t {
|
||||
.client = {
|
||||
.uniqueID = "1234",
|
||||
.cert = PUBLIC_CERT,
|
||||
.name = "test" },
|
||||
.async_insert_pin = { .salt = "ff5dc6eda99339a8a0793e216c4257c4" } }),
|
||||
.override_server_challenge = util::from_hex_vec("AAAAAAAAAAAAAAAA", true),
|
||||
.pin = "5338",
|
||||
/* AES("CLIENT CHALLENGE") */
|
||||
.client_challenge = util::from_hex_vec("741CD3D6890C16DA39D53BCA0893AAF0", true),
|
||||
/* SHA = SHA265(server_challenge + public cert signature + "SECRET ") = "6493DAE49C913E1AEAF37C1072F71D664B72B2C4DA1FFB4720BECE0D929E008A"
|
||||
* AES( SHA ) */
|
||||
.server_challenge_resp = util::from_hex_vec("920BABAE9F7599AA1CA8EC87FB3454C91872A7D8D5127DDC176C2FDAE635CF7A", true),
|
||||
/* secret + x509 signature */
|
||||
.client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFF" // secret
|
||||
"9BB74D8DE2FF006C3F47FC45EFDAA97D433783AFAB3ACD85CA7ED2330BB2A7BD18A5B044AF8CAC177116FAE8A6E8E44653A8944A0F8EA138B2E013756D847D2C4FC52F736E2E7E9B4154712B18F8307B2A161E010F0587744163E42ECA9EA548FC435756EDCF1FEB94037631ABB72B29DDAC0EA5E61F2DBFCC3B20AA021473CC85AC98D88052CA6618ED1701EFBF142C18D5E779A3155B84DF65057D4823EC194E6DF14006793E8D7A3DCCE20A911636C4E01ECA8B54B9DE9F256F15DE9A980EA024B30D77579140D45EC220C738164BDEEEBF7364AE94A5FF9B784B40F2E640CE8603017DEEAC7B2AD77B807C643B7B349C110FE15F94C7B3D37FF15FDFBE26",
|
||||
true) },
|
||||
pairing_output { true, true, true, true }),
|
||||
// Testing that when passing some empty values we aren't triggering any exception
|
||||
std::make_tuple(pairing_input {
|
||||
.session = std::make_shared<pair_session_t>(pair_session_t { .client = {}, .async_insert_pin = { .salt = "ff5dc6eda99339a8a0793e216c4257c4" } }),
|
||||
.override_server_challenge = {},
|
||||
.pin = {},
|
||||
.client_challenge = {},
|
||||
.server_challenge_resp = {},
|
||||
.client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFFxDEADBEEF", true),
|
||||
},
|
||||
// Only phase 4 will fail, when we check what has been exchanged
|
||||
pairing_output { true, true, true, false }),
|
||||
// Testing that when passing some empty values we aren't triggering any exception
|
||||
std::make_tuple(pairing_input {
|
||||
.session = std::make_shared<pair_session_t>(pair_session_t { .client = { .cert = PUBLIC_CERT }, .async_insert_pin = { .salt = "ff5dc6eda99339a8a0793e216c4257c4" } }),
|
||||
.override_server_challenge = {},
|
||||
.pin = {},
|
||||
.client_challenge = {},
|
||||
.server_challenge_resp = {},
|
||||
.client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFFxDEADBEEF", true),
|
||||
},
|
||||
// Only phase 4 will fail, when we check what has been exchanged
|
||||
pairing_output { true, true, true, false })));
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
TestFailingPairing,
|
||||
PairingTest,
|
||||
testing::Values(
|
||||
/**
|
||||
* Wrong PIN
|
||||
*/
|
||||
std::make_tuple(
|
||||
pairing_input {
|
||||
.session = std::make_shared<pair_session_t>(
|
||||
pair_session_t {
|
||||
.client = {
|
||||
.uniqueID = "1234",
|
||||
.cert = PUBLIC_CERT,
|
||||
.name = "test" },
|
||||
.async_insert_pin = { .salt = "ff5dc6eda99339a8a0793e216c4257c4" } }),
|
||||
.override_server_challenge = util::from_hex_vec("AAAAAAAAAAAAAAAA", true),
|
||||
.pin = "0000",
|
||||
.client_challenge = util::from_hex_vec("741CD3D6890C16DA39D53BCA0893AAF0", true),
|
||||
.server_challenge_resp = util::from_hex_vec("920BABAE9F7599AA1CA8EC87FB3454C91872A7D8D5127DDC176C2FDAE635CF7A", true),
|
||||
.client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFF" // secret
|
||||
"9BB74D8DE2FF006C3F47FC45EFDAA97D433783AFAB3ACD85CA7ED2330BB2A7BD18A5B044AF8CAC177116FAE8A6E8E44653A8944A0F8EA138B2E013756D847D2C4FC52F736E2E7E9B4154712B18F8307B2A161E010F0587744163E42ECA9EA548FC435756EDCF1FEB94037631ABB72B29DDAC0EA5E61F2DBFCC3B20AA021473CC85AC98D88052CA6618ED1701EFBF142C18D5E779A3155B84DF65057D4823EC194E6DF14006793E8D7A3DCCE20A911636C4E01ECA8B54B9DE9F256F15DE9A980EA024B30D77579140D45EC220C738164BDEEEBF7364AE94A5FF9B784B40F2E640CE8603017DEEAC7B2AD77B807C643B7B349C110FE15F94C7B3D37FF15FDFBE26",
|
||||
true) },
|
||||
pairing_output { true, true, true, false }),
|
||||
/**
|
||||
* Wrong client challenge
|
||||
*/
|
||||
std::make_tuple(pairing_input { .session = std::make_shared<pair_session_t>(pair_session_t { .client = { .uniqueID = "1234", .cert = PUBLIC_CERT, .name = "test" }, .async_insert_pin = { .salt = "ff5dc6eda99339a8a0793e216c4257c4" } }), .override_server_challenge = util::from_hex_vec("AAAAAAAAAAAAAAAA", true), .pin = "5338", .client_challenge = util::from_hex_vec("741CD3D6890C16DA39D53BCA0893AAF0", true), .server_challenge_resp = util::from_hex_vec("WRONG", true),
|
||||
.client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFF" // secret
|
||||
"9BB74D8DE2FF006C3F47FC45EFDAA97D433783AFAB3ACD85CA7ED2330BB2A7BD18A5B044AF8CAC177116FAE8A6E8E44653A8944A0F8EA138B2E013756D847D2C4FC52F736E2E7E9B4154712B18F8307B2A161E010F0587744163E42ECA9EA548FC435756EDCF1FEB94037631ABB72B29DDAC0EA5E61F2DBFCC3B20AA021473CC85AC98D88052CA6618ED1701EFBF142C18D5E779A3155B84DF65057D4823EC194E6DF14006793E8D7A3DCCE20A911636C4E01ECA8B54B9DE9F256F15DE9A980EA024B30D77579140D45EC220C738164BDEEEBF7364AE94A5FF9B784B40F2E640CE8603017DEEAC7B2AD77B807C643B7B349C110FE15F94C7B3D37FF15FDFBE26",
|
||||
true) },
|
||||
pairing_output { true, true, true, false }),
|
||||
/**
|
||||
* Wrong signature
|
||||
*/
|
||||
std::make_tuple(pairing_input { .session = std::make_shared<pair_session_t>(pair_session_t { .client = { .uniqueID = "1234", .cert = PUBLIC_CERT, .name = "test" }, .async_insert_pin = { .salt = "ff5dc6eda99339a8a0793e216c4257c4" } }), .override_server_challenge = util::from_hex_vec("AAAAAAAAAAAAAAAA", true), .pin = "5338", .client_challenge = util::from_hex_vec("741CD3D6890C16DA39D53BCA0893AAF0", true), .server_challenge_resp = util::from_hex_vec("920BABAE9F7599AA1CA8EC87FB3454C91872A7D8D5127DDC176C2FDAE635CF7A", true),
|
||||
.client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFF" // secret
|
||||
"NOSIGNATURE", // Wrong signature
|
||||
true) },
|
||||
pairing_output { true, true, true, false }),
|
||||
/**
|
||||
* null values (phase 1)
|
||||
*/
|
||||
std::make_tuple(pairing_input { .session = std::make_shared<pair_session_t>() }, pairing_output { false }),
|
||||
/**
|
||||
* null values (phase 4, phase 2 and 3 have no reason to fail since we are running them in order)
|
||||
*/
|
||||
std::make_tuple(pairing_input { .session = std::make_shared<pair_session_t>(pair_session_t { .async_insert_pin = { .salt = "ff5dc6eda99339a8a0793e216c4257c4" } }) }, pairing_output { true, true, true, false })));
|
||||
|
||||
TEST(PairingTest, OutOfOrderCalls) {
|
||||
boost::property_tree::ptree tree;
|
||||
|
||||
setup(PRIVATE_KEY, PUBLIC_CERT);
|
||||
|
||||
pair_session_t sess {};
|
||||
|
||||
clientchallenge(sess, tree, "test");
|
||||
ASSERT_FALSE(tree.get<int>("root.paired") == 1);
|
||||
|
||||
serverchallengeresp(sess, tree, "test");
|
||||
ASSERT_FALSE(tree.get<int>("root.paired") == 1);
|
||||
|
||||
auto add_cert = std::make_shared<safe::queue_t<crypto::x509_t>>(30);
|
||||
clientpairingsecret(sess, add_cert, tree, "test");
|
||||
ASSERT_FALSE(tree.get<int>("root.paired") == 1);
|
||||
|
||||
// This should work, it's the first time we call it
|
||||
sess.async_insert_pin.salt = "ff5dc6eda99339a8a0793e216c4257c4";
|
||||
getservercert(sess, tree, "test");
|
||||
ASSERT_TRUE(tree.get<int>("root.paired") == 1);
|
||||
|
||||
// Calling it again should fail
|
||||
getservercert(sess, tree, "test");
|
||||
ASSERT_FALSE(tree.get<int>("root.paired") == 1);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user