Files
Sunshine/tests/unit/test_http_pairing.cpp
ABeltramo 89f097ae65
Some checks failed
CI / GitHub Env Debug (push) Waiting to run
CI / Setup Release (push) Waiting to run
CI / Setup Flatpak Matrix (push) Waiting to run
CI / Linux Flatpak (push) Blocked by required conditions
CI / Linux ${{ matrix.type }} (--appimage-build, 22.04, AppImage) (push) Blocked by required conditions
CI / Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) (macos, 13) (push) Blocked by required conditions
CI / Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) (macos, 14) (push) Blocked by required conditions
CI / Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) (ubuntu, latest) (push) Blocked by required conditions
CI / Homebrew (${{ matrix.os_name }}-${{ matrix.os_version }}${{ matrix.release == true && ' (Release)' || '' }}) (ubuntu, latest, true) (push) Blocked by required conditions
CI / Windows (push) Blocked by required conditions
CI Docker / Check Dockerfiles (push) Waiting to run
CI Docker / Setup Release (push) Blocked by required conditions
CI Docker / Docker${{ matrix.tag }} (push) Blocked by required conditions
CodeQL / Get language matrix (push) Waiting to run
CodeQL / Analyze (${{ matrix.name }}) (push) Blocked by required conditions
Build GH-Pages / prep (push) Waiting to run
Build GH-Pages / call-jekyll-build (push) Blocked by required conditions
localize / Update Localization (push) Has been cancelled
Merge commit from fork
Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Co-authored-by: Cameron Gutman <2695644+cgutman@users.noreply.github.com>
2025-01-17 23:17:13 -05:00

211 lines
11 KiB
C++

/**
* @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);
}