From 64a9fc3daef82469dca6fa7dfa7aaab7743233f2 Mon Sep 17 00:00:00 2001 From: Andrey Bychkov Date: Thu, 24 Sep 2026 11:49:36 +0300 Subject: [PATCH] more gtests and fix piauth --- libs/crypt/piauth.cpp | 2 +- tests/CMakeLists.txt | 3 + tests/client_server/client_server_test.cpp | 32 +- tests/crypt/piauth_test.cpp | 512 +++++++++++++++++++++ 4 files changed, 536 insertions(+), 13 deletions(-) create mode 100644 tests/crypt/piauth_test.cpp diff --git a/libs/crypt/piauth.cpp b/libs/crypt/piauth.cpp index 8ce5f329..04ba2d9e 100644 --- a/libs/crypt/piauth.cpp +++ b/libs/crypt/piauth.cpp @@ -231,7 +231,6 @@ PIAuth::State PIAuth::receive(PIByteArray & ba) { return state; } if ((state == KeyExchange && rstate == Connected) || (state == Connected && rstate == Connected)) { - ba.clear(); PIByteArray rinfo; ba >> rinfo; bool ok = false; @@ -239,6 +238,7 @@ PIAuth::State PIAuth::receive(PIByteArray & ba) { if (!ok) return disconnect(ba, "Error while exchange keys"); state = Connected; connected(PIString::fromUTF8(rinfo)); + ba.clear(); ba << (int)state << crypt.generateRandomBuff(randomi() % PIAUTH_NOISE_MAX_SIZE); return state; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index de877a59..90df60e3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -48,3 +48,6 @@ endif() if(TARGET pip_http_server AND TARGET pip_http_client) pip_test(http_server pip_http_server pip_http_client) endif() +if(TARGET pip_crypt) + pip_test(crypt pip_crypt) +endif() diff --git a/tests/client_server/client_server_test.cpp b/tests/client_server/client_server_test.cpp index cb6fc68c..2485f8a5 100644 --- a/tests/client_server/client_server_test.cpp +++ b/tests/client_server/client_server_test.cpp @@ -3,11 +3,19 @@ #include "gtest/gtest.h" +//! \~english Each test binds its own port so parallel ctest shards (which run +//! every TEST case in a separate process) never contend for the same socket. +//! \~russian Каждый тест слушает свой порт, чтобы параллельные ctest shards +//! (запускающие каждый TEST в отдельном процессе) не конфликтовали за сокет. +constexpr ushort OneClientPort = 12345; +constexpr ushort ManyClientsPort = 12346; +constexpr ushort DynamicClientsPort = 12347; + template -PIClientServer::Server * createServer() { +PIClientServer::Server * createServer(ushort port) { auto s = new PIClientServer::Server(); s->setClientFactory([] { return new TestServerClient(); }); - s->listenAll(12345); + s->listenAll(port); return s; } @@ -23,17 +31,17 @@ bool waitLoop(std::function exit_loop, const PISystemTime & timeout) { } template -Client * createAndConnectClient() { +Client * createAndConnectClient(ushort port) { auto c = new Client(); - c->connect(PINetworkAddress::resolve("127.0.0.1", 12345)); + c->connect(PINetworkAddress::resolve("127.0.0.1", port)); return c; } TEST(ClientServer, OneClient) { auto const loop_timeout = 1000_ms; - auto s = createServer(); + auto s = createServer(OneClientPort); piMinSleep(); - auto c = createAndConnectClient>(); + auto c = createAndConnectClient>(OneClientPort); waitLoop([s]() { return s->clientsCount() > 0; }, loop_timeout); EXPECT_EQ(1, s->clientsCount()); @@ -59,8 +67,8 @@ class ClientSendThread { using ClientType = TestClient; public: - ClientSendThread() { - client = createAndConnectClient(); + explicit ClientSendThread(ushort port) { + client = createAndConnectClient(port); sendThread.setName("clSend"); } @@ -106,11 +114,11 @@ TEST(ClientServer, ManyClients) { auto const loop_timeout = 1_s; constexpr int clients_count = 20; PIVector clients; - auto s = createServer(); + auto s = createServer(ManyClientsPort); piMinSleep(); piForTimes(clients_count) { - clients.append(new ClientSendThread()); + clients.append(new ClientSendThread(ManyClientsPort)); } EXPECT_EQ(clients_count, clients.size_s()); @@ -154,11 +162,11 @@ TEST(ClientServer, DynamicClients) { constexpr int clients_count = 20; PIVector clients; PIMutex clients_mutex; - auto s = createServer(); + auto s = createServer(DynamicClientsPort); const auto spawnClient = [&clients, &clients_mutex]() { // if (clients.size() > 100) return; - auto c = new ClientSendThread(); + auto c = new ClientSendThread(DynamicClientsPort); c->startSend(); clients_mutex.lock(); clients << c; diff --git a/tests/crypt/piauth_test.cpp b/tests/crypt/piauth_test.cpp new file mode 100644 index 00000000..ff7df971 --- /dev/null +++ b/tests/crypt/piauth_test.cpp @@ -0,0 +1,512 @@ +#include "piauth.h" +#include "picrypt.h" +#include "piliterals_string.h" + +#include "gtest/gtest.h" + +//! \~english Tests for the PIAuth signed key-exchange state machine. +//! \~russian Тесты машины состояний аутентификации PIAuth с подписанным обменом ключами. +//! +//! The protocol is driven in-process: two \c PIAuth endpoints exchange the same +//! \c PIByteArray packets the network transport would carry. Each \a receive() +//! updates the endpoint state and writes the reply back into the passed buffer. +//! \~russian Протокол прогоняется в одном процессе: две конечные точки \c PIAuth +//! обмениваются теми же пакетами \c PIByteArray, которые переносил бы транспорт. +//! Каждый \a receive() обновляет состояние и записывает ответ обратно в буфер. + + +namespace { + +//! \~english Deterministic signing secret key for a human-readable seed. +//! \~russian Детерминированный секретный ключ подписи по текстовому seed. +PIByteArray signKey(const char * seed) { + return PIAuth::generateSign(PIByteArray::fromAscii(seed)); +} + +//! \~english One-element authorized public key list. +//! \~russian Список доверенных открытых ключей из одного элемента. +PIVector keyList(const PIByteArray & key) { + return PIVector{key}; +} + +} // namespace + + +class PIAuthTest: public ::testing::Test {}; + + +//! \~english When both peers already trust each other's signing key the handshake +//! skips the password stage: probe, key exchange, connected. +//! \~russian Когда узлы уже доверяют ключам подписи друг друга, рукопожатие +//! пропускает этап пароля: probe, обмен ключом, connected. +TEST_F(PIAuthTest, AuthorizedKeysFullHandshake) { + PIAuth client(signKey("client-seed")); + PIAuth server(signKey("server-seed")); + + bool client_connected = false; + bool server_connected = false; + int client_disconnects = 0; + bool password_requested = false; + PIString server_info; + + CONNECTL(&client, connected, [&](PIString) { client_connected = true; }); + CONNECTL(&server, connected, [&](PIString info) { + server_connected = true; + server_info = info; + }); + CONNECTL(&client, disconnected, [&](PIString) { ++client_disconnects; }); + CONNECTL(&client, passwordRequest, [&](PIString * pass) { + password_requested = true; + *pass = "unused"; + }); + + client.setInfoData(PIByteArray::fromAscii("hello-server")); + client.setAuthorizedPublicKeys(keyList(server.getSignPublicKey())); + server.setAuthorizedPublicKeys(keyList(client.getSignPublicKey())); + ASSERT_FALSE(client.getSignPublicKey().isEmpty()); + ASSERT_FALSE(server.getSignPublicKey().isEmpty()); + + client.startClient(); + PIByteArray pkt = server.startServer(); + ASSERT_FALSE(pkt.isEmpty()); + + EXPECT_EQ(PIAuth::AuthReply, client.receive(pkt)); + EXPECT_EQ(PIAuth::KeyExchange, server.receive(pkt)); + EXPECT_EQ(PIAuth::Connected, client.receive(pkt)); + EXPECT_EQ(PIAuth::Connected, server.receive(pkt)); + + EXPECT_TRUE(client_connected); + EXPECT_TRUE(server_connected); + EXPECT_EQ(0, client_disconnects); + EXPECT_FALSE(password_requested); + EXPECT_EQ("hello-server"_a, server_info); + + ASSERT_FALSE(client.getSecretKey().isEmpty()); + EXPECT_EQ(PICrypt::sizeKey(), client.getSecretKey().size()); + EXPECT_EQ(client.getSecretKey(), server.getSecretKey()); +} + + +//! \~english After both peers are Connected an extra round-trip re-sends the +//! application info and keeps the same session key (keep-alive path). +//! \~russian После перехода обоих узлов в Connected дополнительный обмен +//! повторно передает прикладные данные и сохраняет тот же сеансовый ключ +//! (путь keep-alive). +TEST_F(PIAuthTest, ConnectedKeepAliveRoundTrip) { + PIAuth client(signKey("client-seed")); + PIAuth server(signKey("server-seed")); + + int client_connected = 0; + int server_connected = 0; + PIString server_info; + + client.setInfoData(PIByteArray::fromAscii("kept-info")); + client.setAuthorizedPublicKeys(keyList(server.getSignPublicKey())); + server.setAuthorizedPublicKeys(keyList(client.getSignPublicKey())); + CONNECTL(&client, connected, [&](PIString) { ++client_connected; }); + CONNECTL(&server, connected, [&](PIString info) { + ++server_connected; + server_info = info; + }); + + client.startClient(); + PIByteArray pkt = server.startServer(); + EXPECT_EQ(PIAuth::AuthReply, client.receive(pkt)); + EXPECT_EQ(PIAuth::KeyExchange, server.receive(pkt)); + EXPECT_EQ(PIAuth::Connected, client.receive(pkt)); + EXPECT_EQ(PIAuth::Connected, server.receive(pkt)); + + // server.receive() left its keep-alive reply in "pkt". + EXPECT_EQ(PIAuth::Connected, client.receive(pkt)); + EXPECT_EQ(PIAuth::Connected, server.receive(pkt)); + + EXPECT_EQ(2, client_connected); + EXPECT_EQ(2, server_connected); + EXPECT_EQ("kept-info"_a, server_info); + EXPECT_EQ(client.getSecretKey(), server.getSecretKey()); +} + + +//! \~english With no mutual trust the client approves the server via the +//! \a authorize event, the server requests a password and both reach Connected +//! with the same session key. +//! \~russian Без взаимного доверия клиент подтверждает сервер через событие +//! \a authorize, сервер запрашивает пароль, и обе стороны доходят до Connected +//! с одинаковым сеансовым ключом. +TEST_F(PIAuthTest, PasswordFullHandshake) { + PIAuth client(signKey("client-seed")); + PIAuth server(signKey("server-seed")); + + bool authorized = false; + bool password_asked = false; + bool password_checked = false; + bool password_ok = false; + int client_disconnects = 0; + PIString server_info; + + server.setServerPassword("s3cret"); + client.setInfoData(PIByteArray::fromAscii("my-info")); + + CONNECTL(&client, authorize, [&](PIByteArray, bool * ok) { + authorized = true; + *ok = true; + }); + CONNECTL(&client, passwordRequest, [&](PIString * pass) { + password_asked = true; + *pass = "s3cret"; + }); + CONNECTL(&server, passwordCheck, [&](bool result) { + password_checked = true; + password_ok = result; + }); + CONNECTL(&client, disconnected, [&](PIString) { ++client_disconnects; }); + CONNECTL(&client, connected, [&](PIString) {}); + CONNECTL(&server, connected, [&](PIString info) { server_info = info; }); + + client.startClient(); + PIByteArray pkt = server.startServer(); + + EXPECT_EQ(PIAuth::AuthReply, client.receive(pkt)); + EXPECT_EQ(PIAuth::PassRequest, server.receive(pkt)); + EXPECT_EQ(PIAuth::PassRequest, client.receive(pkt)); + EXPECT_EQ(PIAuth::KeyExchange, server.receive(pkt)); + EXPECT_EQ(PIAuth::Connected, client.receive(pkt)); + EXPECT_EQ(PIAuth::Connected, server.receive(pkt)); + + EXPECT_TRUE(authorized); + EXPECT_TRUE(password_asked); + EXPECT_TRUE(password_checked); + EXPECT_TRUE(password_ok); + EXPECT_EQ(0, client_disconnects); + EXPECT_EQ("my-info"_a, server_info); + ASSERT_FALSE(client.getSecretKey().isEmpty()); + EXPECT_EQ(client.getSecretKey(), server.getSecretKey()); +} + + +//! \~english A client that already trusts the server key still has to pass the +//! server password check when the server does not know the client key. The +//! \a authorize event must not fire in that case. +//! \~russian Клиент, уже доверяющий ключу сервера, всё равно проходит проверку +//! пароля, если сервер не знает ключ клиента. Событие \a authorize не должно +//! срабатывать. +TEST_F(PIAuthTest, TrustedServerPasswordStage) { + PIAuth client(signKey("client-seed")); + PIAuth server(signKey("server-seed")); + + bool authorized = false; + bool password_asked = false; + + server.setServerPassword("pw"); + client.setAuthorizedPublicKeys(keyList(server.getSignPublicKey())); + CONNECTL(&client, authorize, [&](PIByteArray, bool * ok) { + authorized = true; + *ok = true; + }); + CONNECTL(&client, passwordRequest, [&](PIString * pass) { + password_asked = true; + *pass = "pw"; + }); + CONNECTL(&server, passwordCheck, [](bool) {}); + CONNECTL(&client, connected, [](PIString) {}); + CONNECTL(&server, connected, [](PIString) {}); + + client.startClient(); + PIByteArray pkt = server.startServer(); + + EXPECT_EQ(PIAuth::AuthReply, client.receive(pkt)); + EXPECT_EQ(PIAuth::PassRequest, server.receive(pkt)); + EXPECT_EQ(PIAuth::PassRequest, client.receive(pkt)); + EXPECT_EQ(PIAuth::KeyExchange, server.receive(pkt)); + EXPECT_EQ(PIAuth::Connected, client.receive(pkt)); + EXPECT_EQ(PIAuth::Connected, server.receive(pkt)); + + EXPECT_FALSE(authorized); + EXPECT_TRUE(password_asked); + EXPECT_FALSE(client.getSecretKey().isEmpty()); + EXPECT_EQ(client.getSecretKey(), server.getSecretKey()); +} + + +//! \~english A wrong password must abort the handshake on the server side and +//! report a failed password check. +//! \~russian Неверный пароль должен прервать рукопожатие на сервере и сообщить +//! о неудачной проверке пароля. +TEST_F(PIAuthTest, WrongPasswordDisconnects) { + PIAuth client(signKey("client-seed")); + PIAuth server(signKey("server-seed")); + + bool password_checked = false; + bool password_ok = true; + PIString disconnect_reason; + + server.setServerPassword("right"); + CONNECTL(&client, authorize, [](PIByteArray, bool * ok) { *ok = true; }); + CONNECTL(&client, passwordRequest, [](PIString * pass) { *pass = "wrong"; }); + CONNECTL(&server, passwordCheck, [&](bool result) { + password_checked = true; + password_ok = result; + }); + CONNECTL(&server, disconnected, [&](PIString reason) { disconnect_reason = reason; }); + CONNECTL(&client, connected, [](PIString) {}); + CONNECTL(&server, connected, [](PIString) {}); + + client.startClient(); + PIByteArray pkt = server.startServer(); + + EXPECT_EQ(PIAuth::AuthReply, client.receive(pkt)); + EXPECT_EQ(PIAuth::PassRequest, server.receive(pkt)); + EXPECT_EQ(PIAuth::PassRequest, client.receive(pkt)); + EXPECT_EQ(PIAuth::NotConnected, server.receive(pkt)); + + EXPECT_TRUE(password_checked); + EXPECT_FALSE(password_ok); + EXPECT_TRUE(disconnect_reason.contains("Invalid password")); + EXPECT_TRUE(server.getSecretKey().isEmpty()); + EXPECT_TRUE(client.getSecretKey().isEmpty()); +} + + +//! \~english The server refuses password authentication when no password is +//! configured, even if an empty hash is submitted. +//! \~russian Сервер отказывает в парольной аутентификации, если пароль не задан, +//! даже если передан пустой хеш. +TEST_F(PIAuthTest, EmptyServerPasswordRejects) { + PIAuth client(signKey("client-seed")); + PIAuth server(signKey("server-seed")); + + bool password_ok = true; + CONNECTL(&client, authorize, [](PIByteArray, bool * ok) { *ok = true; }); + CONNECTL(&client, passwordRequest, [](PIString * pass) { *pass = "anything"; }); + CONNECTL(&server, passwordCheck, [&](bool result) { password_ok = result; }); + CONNECTL(&client, connected, [](PIString) {}); + CONNECTL(&server, connected, [](PIString) {}); + + client.startClient(); + PIByteArray pkt = server.startServer(); + + EXPECT_EQ(PIAuth::AuthReply, client.receive(pkt)); + EXPECT_EQ(PIAuth::PassRequest, server.receive(pkt)); + EXPECT_EQ(PIAuth::PassRequest, client.receive(pkt)); + EXPECT_EQ(PIAuth::NotConnected, server.receive(pkt)); + EXPECT_FALSE(password_ok); +} + + +//! \~english Rejecting an unknown server in the \a authorize callback aborts the +//! handshake on the client. +//! \~russian Отказ от неизвестного сервера в обработчике \a authorize прерывает +//! рукопожатие на клиенте. +TEST_F(PIAuthTest, AuthorizeRejectedDisconnects) { + PIAuth client(signKey("client-seed")); + PIAuth server(signKey("server-seed")); + + bool authorize_called = false; + PIString disconnect_reason; + + CONNECTL(&client, authorize, [&](PIByteArray, bool * ok) { + authorize_called = true; + *ok = false; + }); + CONNECTL(&client, disconnected, [&](PIString reason) { disconnect_reason = reason; }); + CONNECTL(&client, connected, [](PIString) {}); + + client.startClient(); + PIByteArray pkt = server.startServer(); + + EXPECT_EQ(PIAuth::NotConnected, client.receive(pkt)); + EXPECT_TRUE(authorize_called); + EXPECT_TRUE(disconnect_reason.contains("Unauthorised")); + EXPECT_TRUE(client.getSecretKey().isEmpty()); +} + + +//! \~english An empty password from the client callback cancels the handshake. +//! \~russian Пустой пароль из клиентского обработчика отменяет рукопожатие. +TEST_F(PIAuthTest, PasswordRequestCancelledDisconnects) { + PIAuth client(signKey("client-seed")); + PIAuth server(signKey("server-seed")); + + PIString disconnect_reason; + + server.setServerPassword("pw"); + CONNECTL(&client, authorize, [](PIByteArray, bool * ok) { *ok = true; }); + CONNECTL(&client, passwordRequest, [](PIString * pass) { pass->clear(); }); + CONNECTL(&client, disconnected, [&](PIString reason) { disconnect_reason = reason; }); + CONNECTL(&client, connected, [](PIString) {}); + CONNECTL(&server, connected, [](PIString) {}); + + client.startClient(); + PIByteArray pkt = server.startServer(); + + EXPECT_EQ(PIAuth::AuthReply, client.receive(pkt)); + EXPECT_EQ(PIAuth::PassRequest, server.receive(pkt)); + EXPECT_EQ(PIAuth::NotConnected, client.receive(pkt)); + EXPECT_TRUE(disconnect_reason.contains("Canceled by user")); +} + + +//! \~english A tampered server probe fails signature verification on the client. +//! \~russian Измененный пакет-проба сервера не проходит проверку подписи на клиенте. +TEST_F(PIAuthTest, TamperedProbeRejected) { + PIAuth client(signKey("client-seed")); + PIAuth server(signKey("server-seed")); + + PIString disconnect_reason; + CONNECTL(&client, authorize, [](PIByteArray, bool * ok) { *ok = true; }); + CONNECTL(&client, disconnected, [&](PIString reason) { disconnect_reason = reason; }); + CONNECTL(&client, connected, [](PIString) {}); + + client.startClient(); + PIByteArray pkt = server.startServer(); + ASSERT_TRUE(pkt.size() > 0); + pkt[pkt.size() - 1] ^= 0xFF; + + EXPECT_EQ(PIAuth::NotConnected, client.receive(pkt)); + EXPECT_TRUE(disconnect_reason.contains("Incorrect sign")); +} + + +//! \~english A too-short packet is rejected without touching the session. +//! \~russian Слишком короткий пакет отклоняется без изменения сессии. +TEST_F(PIAuthTest, InvalidDataSizeRejected) { + PIAuth client(signKey("client-seed")); + + PIString disconnect_reason; + CONNECTL(&client, disconnected, [&](PIString reason) { disconnect_reason = reason; }); + + client.startClient(); + PIByteArray empty; + EXPECT_EQ(PIAuth::NotConnected, client.receive(empty)); + EXPECT_TRUE(disconnect_reason.contains("invalid data size")); + + PIByteArray short_pkt; + short_pkt << (uchar)1 << (uchar)2; + EXPECT_EQ(PIAuth::NotConnected, client.receive(short_pkt)); +} + + +//! \~english A packet whose state does not match the current one is rejected. +//! \~russian Пакет, состояние которого не соответствует текущему, отклоняется. +TEST_F(PIAuthTest, UnexpectedStateRejected) { + PIAuth client(signKey("client-seed")); + + PIString disconnect_reason; + CONNECTL(&client, disconnected, [&](PIString reason) { disconnect_reason = reason; }); + + client.startClient(); + PIByteArray pkt; + pkt << (int)PIAuth::PassRequest << PIByteArray::fromAscii("junk"); + EXPECT_EQ(PIAuth::NotConnected, client.receive(pkt)); + EXPECT_TRUE(disconnect_reason.contains("invalid state")); +} + + +//! \~english Before the handshake no session key is available; \a stop() clears an +//! established one and the endpoint can handshake again afterwards. +//! \~russian До рукопожатия сеансовый ключ недоступен; \a stop() очищает +//! установленный ключ, после чего конечная точка может пройти рукопожатие снова. +TEST_F(PIAuthTest, SecretKeyAvailableOnlyWhenConnected) { + PIAuth client(signKey("client-seed")); + PIAuth server(signKey("server-seed")); + + client.setAuthorizedPublicKeys(keyList(server.getSignPublicKey())); + server.setAuthorizedPublicKeys(keyList(client.getSignPublicKey())); + CONNECTL(&client, connected, [](PIString) {}); + CONNECTL(&server, connected, [](PIString) {}); + + client.startClient(); + EXPECT_TRUE(client.getSecretKey().isEmpty()); + + PIByteArray pkt = server.startServer(); + EXPECT_EQ(PIAuth::AuthReply, client.receive(pkt)); + EXPECT_TRUE(client.getSecretKey().isEmpty()); + EXPECT_EQ(PIAuth::KeyExchange, server.receive(pkt)); + EXPECT_EQ(PIAuth::Connected, client.receive(pkt)); + ASSERT_FALSE(client.getSecretKey().isEmpty()); + + client.stop(); + EXPECT_TRUE(client.getSecretKey().isEmpty()); + + // A stopped client can run a fresh handshake with a new server. + PIAuth server2(signKey("server-seed-2")); + client.setAuthorizedPublicKeys(keyList(server2.getSignPublicKey())); + server2.setAuthorizedPublicKeys(keyList(client.getSignPublicKey())); + CONNECTL(&server2, connected, [](PIString) {}); + client.startClient(); + pkt = server2.startServer(); + EXPECT_EQ(PIAuth::AuthReply, client.receive(pkt)); + EXPECT_EQ(PIAuth::KeyExchange, server2.receive(pkt)); + EXPECT_EQ(PIAuth::Connected, client.receive(pkt)); + EXPECT_EQ(PIAuth::Connected, server2.receive(pkt)); + ASSERT_FALSE(client.getSecretKey().isEmpty()); + EXPECT_EQ(client.getSecretKey(), server2.getSecretKey()); +} + + +//! \~english Two independent handshakes must derive different session keys. +//! \~russian Два независимых рукопожатия должны получить разные сеансовые ключи. +TEST_F(PIAuthTest, SessionKeysAreUnique) { + auto handshake = [](const PIByteArray & client_key, const PIByteArray & server_key) { + PIAuth client(client_key); + PIAuth server(server_key); + client.setAuthorizedPublicKeys(keyList(server.getSignPublicKey())); + server.setAuthorizedPublicKeys(keyList(client.getSignPublicKey())); + CONNECTL(&client, connected, [](PIString) {}); + CONNECTL(&server, connected, [](PIString) {}); + client.startClient(); + PIByteArray pkt = server.startServer(); + client.receive(pkt); + server.receive(pkt); + client.receive(pkt); + server.receive(pkt); + return client.getSecretKey(); + }; + + PIByteArray first = handshake(signKey("client-seed"), signKey("server-seed")); + PIByteArray second = handshake(signKey("client-seed"), signKey("server-seed")); + ASSERT_FALSE(first.isEmpty()); + ASSERT_FALSE(second.isEmpty()); + EXPECT_NE(first, second); +} + + +//! \~english \a generateSign() is deterministic and the endpoint derives the +//! matching public key from the secret. +//! \~russian \a generateSign() детерминирован, а конечная точка выводит +//! соответствующий открытый ключ из секретного. +TEST_F(PIAuthTest, GenerateSignAndPublicKey) { + PIByteArray sk1 = PIAuth::generateSign(PIByteArray::fromAscii("seed")); + PIByteArray sk2 = PIAuth::generateSign(PIByteArray::fromAscii("seed")); + PIByteArray sk3 = PIAuth::generateSign(PIByteArray::fromAscii("other-seed")); + + EXPECT_FALSE(sk1.isEmpty()); + EXPECT_EQ(sk1, sk2); + EXPECT_NE(sk1, sk3); + + PIAuth endpoint(sk1); + EXPECT_EQ(PICrypt::extractSignPublicKey(sk1), endpoint.getSignPublicKey()); + EXPECT_FALSE(endpoint.getSignPublicKey().isEmpty()); +} + + +//! \~english The authorized key list is exposed and replaced through the accessors. +//! \~russian Список доверенных ключей читается и заменяется через аксессоры. +TEST_F(PIAuthTest, AuthorizedKeysAccessors) { + PIAuth endpoint(signKey("seed")); + PIByteArray first = signKey("first"); + PIByteArray second = signKey("second"); + + EXPECT_TRUE(endpoint.getAuthorizedPublicKeys().isEmpty()); + endpoint.setAuthorizedPublicKeys(keyList(first)); + ASSERT_EQ(1, endpoint.getAuthorizedPublicKeys().size()); + EXPECT_EQ(first, endpoint.getAuthorizedPublicKeys()[0]); + + endpoint.setAuthorizedPublicKeys(PIVector{first, second}); + ASSERT_EQ(2, endpoint.getAuthorizedPublicKeys().size()); + EXPECT_EQ(first, endpoint.getAuthorizedPublicKeys()[0]); + EXPECT_EQ(second, endpoint.getAuthorizedPublicKeys()[1]); + + endpoint.setAuthorizedPublicKeys(PIVector()); + EXPECT_TRUE(endpoint.getAuthorizedPublicKeys().isEmpty()); +}