feat: port GIP protocol and auth stack
Port the GIP protocol and authentication layers from medusalix/xone into the C++ stack: - crypto: SHA-256/HMAC (CommonCrypto), RSA PKCS#1 (Security.framework), and a self-contained P-256 ECDH validated against OpenSSL vectors - auth: v1 (RSA) and v2 (ECDH) handshake state machine - gip: header/varint/chunk handling and packet dispatch, with the kernel device model replaced by transport + client_listener interfaces Adds test_crypto, test_auth, and test_gip suites. Co-Authored-By: deepseek (deepseek/deepseek-v4-pro-0813): ported crypto, auth, and GIP
This commit is contained in:
@@ -15,6 +15,27 @@ target_compile_features(test_types PRIVATE cxx_std_23)
|
||||
add_test(NAME test_types COMMAND test_types)
|
||||
list(APPEND TEST_TARGETS test_types)
|
||||
|
||||
add_executable(test_crypto test_crypto.cpp)
|
||||
target_include_directories(test_crypto PRIVATE "${CMAKE_SOURCE_DIR}/include")
|
||||
target_link_libraries(test_crypto PRIVATE xone_auth Unity::Unity)
|
||||
target_compile_features(test_crypto PRIVATE cxx_std_23)
|
||||
add_test(NAME test_crypto COMMAND test_crypto)
|
||||
list(APPEND TEST_TARGETS test_crypto)
|
||||
|
||||
add_executable(test_auth test_auth.cpp)
|
||||
target_include_directories(test_auth PRIVATE "${CMAKE_SOURCE_DIR}/include")
|
||||
target_link_libraries(test_auth PRIVATE xone_auth Unity::Unity)
|
||||
target_compile_features(test_auth PRIVATE cxx_std_23)
|
||||
add_test(NAME test_auth COMMAND test_auth)
|
||||
list(APPEND TEST_TARGETS test_auth)
|
||||
|
||||
add_executable(test_gip test_gip.cpp)
|
||||
target_include_directories(test_gip PRIVATE "${CMAKE_SOURCE_DIR}/include")
|
||||
target_link_libraries(test_gip PRIVATE xone_gip Unity::Unity)
|
||||
target_compile_features(test_gip PRIVATE cxx_std_23)
|
||||
add_test(NAME test_gip COMMAND test_gip)
|
||||
list(APPEND TEST_TARGETS test_gip)
|
||||
|
||||
add_custom_target(check
|
||||
COMMAND ${CMAKE_CTEST_COMMAND}
|
||||
--test-dir "${CMAKE_BINARY_DIR}"
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
#include "unity.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <Security/Security.h>
|
||||
|
||||
#include "auth/auth.hpp"
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
namespace {
|
||||
|
||||
using u8 = std::uint8_t;
|
||||
using u16 = std::uint16_t;
|
||||
|
||||
class mock_sink : public xone::auth::auth_sink {
|
||||
public:
|
||||
struct sent {
|
||||
std::vector<u8> data;
|
||||
bool ack;
|
||||
};
|
||||
|
||||
std::vector<sent> packets;
|
||||
std::vector<u8> key;
|
||||
|
||||
auto send(std::span<u8 const> pkt, bool ack) -> int override
|
||||
{
|
||||
packets.push_back({std::vector<u8>(pkt.begin(), pkt.end()), ack});
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto set_encryption_key(std::span<u8 const> k) -> int override
|
||||
{
|
||||
key.assign(k.begin(), k.end());
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Build an auth data packet: full header + payload.
|
||||
auto make_pkt(u8 cmd, std::vector<u8> const& payload) -> std::vector<u8>
|
||||
{
|
||||
std::vector<u8> p;
|
||||
u16 data_len = static_cast<u16>(payload.size());
|
||||
|
||||
p.push_back(0x00); // context = handshake
|
||||
p.push_back(0x00); // options (client -> host)
|
||||
p.push_back(0x00); // error
|
||||
p.push_back(cmd);
|
||||
p.push_back(static_cast<u8>(data_len >> 8));
|
||||
p.push_back(static_cast<u8>(data_len & 0xFF));
|
||||
|
||||
p.push_back(cmd); // data.command
|
||||
p.push_back(0x01); // data.version
|
||||
u16 data_body = static_cast<u16>(data_len - 4);
|
||||
p.push_back(static_cast<u8>(data_body >> 8));
|
||||
p.push_back(static_cast<u8>(data_body & 0xFF));
|
||||
|
||||
p.insert(p.end(), payload.begin(), payload.end());
|
||||
return p;
|
||||
}
|
||||
|
||||
auto make_rsa_der() -> std::vector<u8>
|
||||
{
|
||||
CFTypeRef keys[] = {kSecAttrKeyType, kSecAttrKeySizeInBits,
|
||||
kSecAttrIsPermanent};
|
||||
CFNumberRef size = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType,
|
||||
(int[]){2048});
|
||||
CFTypeRef vals[] = {kSecAttrKeyTypeRSA, size, kCFBooleanFalse};
|
||||
CFDictionaryRef attrs = CFDictionaryCreate(
|
||||
kCFAllocatorDefault, keys, vals, 3, &kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks);
|
||||
CFErrorRef error = nullptr;
|
||||
SecKeyRef priv = SecKeyCreateRandomKey(attrs, &error);
|
||||
SecKeyRef pub = SecKeyCopyPublicKey(priv);
|
||||
CFDataRef ext = SecKeyCopyExternalRepresentation(pub, &error);
|
||||
std::vector<u8> der(CFDataGetLength(ext));
|
||||
CFDataGetBytes(ext, CFRangeMake(0, CFDataGetLength(ext)), der.data());
|
||||
CFRelease(ext);
|
||||
CFRelease(pub);
|
||||
CFRelease(priv);
|
||||
CFRelease(size);
|
||||
CFRelease(attrs);
|
||||
return der;
|
||||
}
|
||||
|
||||
auto be16_at(std::vector<u8> const& v, std::size_t i) -> u16
|
||||
{
|
||||
return static_cast<u16>((v[i] << 8) | v[i + 1]);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void test_start_sends_host_hello(void)
|
||||
{
|
||||
mock_sink sink;
|
||||
xone::auth::auth auth{sink};
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, auth.start());
|
||||
TEST_ASSERT_EQUAL_INT(1, sink.packets.size());
|
||||
TEST_ASSERT_TRUE(sink.packets[0].ack);
|
||||
TEST_ASSERT_EQUAL_INT(58, sink.packets[0].data.size());
|
||||
|
||||
auto const& p = sink.packets[0].data;
|
||||
TEST_ASSERT_EQUAL_UINT8(0x00, p[0]); // context
|
||||
TEST_ASSERT_EQUAL_UINT8(0x41, p[1]); // ACK | FROM_HOST
|
||||
TEST_ASSERT_EQUAL_UINT8(0x01, p[3]); // host hello
|
||||
TEST_ASSERT_EQUAL_INT(44, be16_at(p, 4));
|
||||
TEST_ASSERT_EQUAL_UINT8(0x01, p[6]); // data.command
|
||||
TEST_ASSERT_EQUAL_UINT8(0x01, p[7]); // data.version
|
||||
TEST_ASSERT_EQUAL_INT(40, be16_at(p, 8));
|
||||
}
|
||||
|
||||
void test_client_hello_requests_certificate(void)
|
||||
{
|
||||
mock_sink sink;
|
||||
xone::auth::auth auth{sink};
|
||||
auth.start();
|
||||
std::size_t base = sink.packets.size();
|
||||
|
||||
std::vector<u8> payload(80, 0x11); // client hello: random + unknown
|
||||
TEST_ASSERT_EQUAL_INT(0, auth.process_pkt(make_pkt(0x02, payload)));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(base + 1, sink.packets.size());
|
||||
auto const& p = sink.packets.back().data;
|
||||
TEST_ASSERT_EQUAL_INT(14, p.size()); // request packet
|
||||
TEST_ASSERT_TRUE(sink.packets.back().ack);
|
||||
TEST_ASSERT_EQUAL_UINT8(0x42, p[1]); // REQUEST | FROM_HOST
|
||||
TEST_ASSERT_EQUAL_UINT8(0x03, p[3]); // client certificate
|
||||
TEST_ASSERT_EQUAL_INT(1028, be16_at(p, 4)); // cert max len + data hdr
|
||||
}
|
||||
|
||||
void test_certificate_triggers_rsa_exchange(void)
|
||||
{
|
||||
mock_sink sink;
|
||||
xone::auth::auth auth{sink};
|
||||
auth.start();
|
||||
|
||||
// client hello -> cert request
|
||||
auth.process_pkt(make_pkt(0x02, std::vector<u8>(80, 0x22)));
|
||||
std::size_t base = sink.packets.size();
|
||||
|
||||
// certificate: some prefix bytes + ASN.1 SEQUENCE + 270-byte DER
|
||||
std::vector<u8> der = make_rsa_der();
|
||||
TEST_ASSERT_EQUAL_INT(270, der.size());
|
||||
TEST_ASSERT_EQUAL_UINT8(0x30, der[0]);
|
||||
TEST_ASSERT_EQUAL_UINT8(0x82, der[1]);
|
||||
TEST_ASSERT_EQUAL_UINT8(0x01, der[2]);
|
||||
TEST_ASSERT_EQUAL_UINT8(0x0a, der[3]);
|
||||
|
||||
std::vector<u8> cert(64, 0x33);
|
||||
cert.insert(cert.end(), der.begin(), der.end());
|
||||
cert.insert(cert.end(), 32, 0x44);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, auth.process_pkt(make_pkt(0x03, cert)));
|
||||
|
||||
// host secret packet
|
||||
TEST_ASSERT_EQUAL_INT(base + 1, sink.packets.size());
|
||||
auto const& p = sink.packets.back().data;
|
||||
TEST_ASSERT_EQUAL_INT(274, p.size()); // header_full + 256 + trailer
|
||||
TEST_ASSERT_TRUE(sink.packets.back().ack);
|
||||
TEST_ASSERT_EQUAL_UINT8(0x05, p[3]); // host secret
|
||||
}
|
||||
|
||||
void test_acknowledge_sends_host_finish(void)
|
||||
{
|
||||
mock_sink sink;
|
||||
xone::auth::auth auth{sink};
|
||||
auth.start();
|
||||
auth.process_pkt(make_pkt(0x02, std::vector<u8>(80, 0x22)));
|
||||
|
||||
std::vector<u8> der = make_rsa_der();
|
||||
std::vector<u8> cert(der.begin(), der.end());
|
||||
auth.process_pkt(make_pkt(0x03, cert));
|
||||
std::size_t base = sink.packets.size();
|
||||
|
||||
// ACK: handshake header with options=ACK, command=0x01
|
||||
std::vector<u8> ack{0x00, 0x01, 0x00, 0x01, 0x00, 0x00};
|
||||
TEST_ASSERT_EQUAL_INT(0, auth.process_pkt(ack));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(base + 1, sink.packets.size());
|
||||
auto const& p = sink.packets.back().data;
|
||||
TEST_ASSERT_EQUAL_INT(50, p.size()); // header_full + 32 + trailer
|
||||
TEST_ASSERT_EQUAL_UINT8(0x07, p[3]); // host finish
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_start_sends_host_hello);
|
||||
RUN_TEST(test_client_hello_requests_certificate);
|
||||
RUN_TEST(test_certificate_triggers_rsa_exchange);
|
||||
RUN_TEST(test_acknowledge_sends_host_finish);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
#include "unity.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <Security/Security.h>
|
||||
|
||||
#include "auth/crypto.hpp"
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
namespace {
|
||||
|
||||
using u8 = std::uint8_t;
|
||||
|
||||
auto hex(std::string_view s) -> std::vector<u8>
|
||||
{
|
||||
std::vector<u8> out;
|
||||
for (std::size_t i = 0; i + 1 < s.size(); i += 2) {
|
||||
char buf[3] = {s[i], s[i + 1], 0};
|
||||
out.push_back(static_cast<u8>(std::strtoul(buf, nullptr, 16)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
template <std::size_t N>
|
||||
auto to_array(std::vector<u8> const& v) -> std::array<u8, N>
|
||||
{
|
||||
std::array<u8, N> a{};
|
||||
TEST_ASSERT(v.size() == N);
|
||||
std::copy(v.begin(), v.end(), a.begin());
|
||||
return a;
|
||||
}
|
||||
|
||||
auto span(std::vector<u8> const& v) -> std::span<u8 const>
|
||||
{
|
||||
return {v.data(), v.size()};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// SHA-256
|
||||
// --------------------------------------------------------------------------
|
||||
void test_sha256_abc(void)
|
||||
{
|
||||
std::array<u8, 32> out{};
|
||||
xone::auth::sha256 h;
|
||||
std::string data = "abc";
|
||||
h.update({reinterpret_cast<u8 const*>(data.data()), data.size()});
|
||||
h.finalize(out);
|
||||
auto expect = hex("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expect.data(), out.data(), 32);
|
||||
}
|
||||
|
||||
void test_sha256_snapshot(void)
|
||||
{
|
||||
// snapshot must not disturb continued hashing
|
||||
std::array<u8, 32> out{};
|
||||
xone::auth::sha256 h;
|
||||
std::string a = "abc";
|
||||
h.update({reinterpret_cast<u8 const*>(a.data()), a.size()});
|
||||
|
||||
xone::auth::sha256 snap = h; // snapshot
|
||||
std::string b = "def";
|
||||
h.update({reinterpret_cast<u8 const*>(b.data()), b.size()});
|
||||
h.finalize(out);
|
||||
auto expect = hex("bef57ec7f53a6d40beb640a780a639c83bc29ac8a9816f1fc6c5c6dcd93c4721");
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expect.data(), out.data(), 32);
|
||||
|
||||
// snapshot still hashes only "abc"
|
||||
std::array<u8, 32> snap_out{};
|
||||
snap.finalize(snap_out);
|
||||
auto expect_snap = hex("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expect_snap.data(), snap_out.data(), 32);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// HMAC-SHA256 (RFC 4231)
|
||||
// --------------------------------------------------------------------------
|
||||
void test_hmac_rfc4231_case2(void)
|
||||
{
|
||||
// key = "Jefe", data = "what do ya want for nothing?"
|
||||
std::string key = "Jefe";
|
||||
std::string data = "what do ya want for nothing?";
|
||||
xone::auth::hmac_sha256 h{{reinterpret_cast<u8 const*>(key.data()), key.size()}};
|
||||
h.update({reinterpret_cast<u8 const*>(data.data()), data.size()});
|
||||
std::array<u8, 32> out{};
|
||||
h.finalize(out);
|
||||
auto expect = hex("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843");
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expect.data(), out.data(), 32);
|
||||
}
|
||||
|
||||
void test_hmac_rfc4231_case1(void)
|
||||
{
|
||||
// key = 0x0b * 20, data = "Hi There"
|
||||
std::vector<u8> key(20, 0x0b);
|
||||
std::string data = "Hi There";
|
||||
xone::auth::hmac_sha256 h{span(key)};
|
||||
h.update({reinterpret_cast<u8 const*>(data.data()), data.size()});
|
||||
std::array<u8, 32> out{};
|
||||
h.finalize(out);
|
||||
auto expect = hex("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7");
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expect.data(), out.data(), 32);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// PRF (TLS P_SHA256)
|
||||
// --------------------------------------------------------------------------
|
||||
void test_prf_expansion(void)
|
||||
{
|
||||
// Long output forces multiple blocks; verify against Python-computed value.
|
||||
std::vector<u8> key = hex("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff");
|
||||
std::vector<u8> seed = hex("deadbeefcafebabe");
|
||||
std::array<u8, 48> out{};
|
||||
xone::auth::prf_sha256(span(key), "Master Secret", span(seed), out);
|
||||
auto expect = hex(
|
||||
"78b92e5b14f8b98dc9f5ddd33668d3dd19e74467f38992009d71bdfd69b5dfa2"
|
||||
"8ff26eaad240df9fe705798d6af784d8");
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expect.data(), out.data(), 48);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ECDH P-256
|
||||
// --------------------------------------------------------------------------
|
||||
void test_ec_small_multiples(void)
|
||||
{
|
||||
auto two = hex("0000000000000000000000000000000000000000000000000000000000000002");
|
||||
auto three = hex("0000000000000000000000000000000000000000000000000000000000000003");
|
||||
|
||||
xone::auth::ec_point q{};
|
||||
xone::auth::ec_base_point_multiply(to_array<32>(two), q);
|
||||
auto expect2g = hex(
|
||||
"7cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc47669978"
|
||||
"07775510db8ed040293d9ac69f7430dbba7dade63ce982299e04b79d227873d1");
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expect2g.data(), q.data(), 64);
|
||||
|
||||
xone::auth::ec_base_point_multiply(to_array<32>(three), q);
|
||||
auto expect3g = hex(
|
||||
"5ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c"
|
||||
"8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d5032");
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expect3g.data(), q.data(), 64);
|
||||
}
|
||||
|
||||
void test_ec_base_multiply(void)
|
||||
{
|
||||
// vectors generated with OpenSSL (see test comments in commit)
|
||||
auto d = hex("ca9aca63d14014baf4e37bcaeb90317557728b479ca42e126fcb9ae9a4f7f765");
|
||||
auto puba = hex(
|
||||
"dca2dafb3baa46602e7512de4690ede383e390148b3c041b34e1beae2b9ec997"
|
||||
"abf83c388bb7b53cd086a8d20810b22ac1f5d92484abf761f53b6725988604cd");
|
||||
|
||||
xone::auth::ec_point q{};
|
||||
xone::auth::ec_base_point_multiply(to_array<32>(d), q);
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(puba.data(), q.data(), 64);
|
||||
}
|
||||
|
||||
void test_ec_shared_secret(void)
|
||||
{
|
||||
auto d = hex("ca9aca63d14014baf4e37bcaeb90317557728b479ca42e126fcb9ae9a4f7f765");
|
||||
auto pubb = hex(
|
||||
"5baeca87ad9b623f7d0cd33af316e5f57d17b711e821acc449a12e5427c95aa3"
|
||||
"a0b86826cf4f028cb90bfd284e99db36cc19c640a914b97ef35dd965e2af19da");
|
||||
auto secret = hex("df01a8c296d70b09a91bc74f4bd25d877cffa0fe43287d24939e44cdc2e9ec25");
|
||||
|
||||
xone::auth::ec_scalar shared{};
|
||||
TEST_ASSERT_TRUE(
|
||||
xone::auth::ec_compute_shared(to_array<32>(d), to_array<64>(pubb), shared));
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(secret.data(), shared.data(), 32);
|
||||
}
|
||||
|
||||
void test_ec_rejects_off_curve_point(void)
|
||||
{
|
||||
auto d = hex("ca9aca63d14014baf4e37bcaeb90317557728b479ca42e126fcb9ae9a4f7f765");
|
||||
// point with x=1, y=1 is not on the curve
|
||||
xone::auth::ec_point bad{};
|
||||
bad[31] = 1;
|
||||
bad[63] = 1;
|
||||
|
||||
xone::auth::ec_scalar shared{};
|
||||
TEST_ASSERT_FALSE(
|
||||
xone::auth::ec_compute_shared(to_array<32>(d), bad, shared));
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// RSA (PKCS#1 v1.5) round-trip against Security.framework
|
||||
// --------------------------------------------------------------------------
|
||||
void test_rsa_roundtrip(void)
|
||||
{
|
||||
CFErrorRef error = nullptr;
|
||||
CFTypeRef attr_keys[] = {kSecAttrKeyType, kSecAttrKeySizeInBits,
|
||||
kSecAttrIsPermanent};
|
||||
CFNumberRef size = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType,
|
||||
(int[]){2048});
|
||||
CFTypeRef attr_vals[] = {kSecAttrKeyTypeRSA, size, kCFBooleanFalse};
|
||||
CFDictionaryRef attrs = CFDictionaryCreate(
|
||||
kCFAllocatorDefault, attr_keys, attr_vals, 3,
|
||||
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
TEST_ASSERT_NOT_NULL(attrs);
|
||||
|
||||
SecKeyRef priv = SecKeyCreateRandomKey(attrs, &error);
|
||||
CFRelease(size);
|
||||
TEST_ASSERT_NOT_NULL(priv);
|
||||
if (!priv) {
|
||||
CFRelease(attrs);
|
||||
return;
|
||||
}
|
||||
|
||||
SecKeyRef pub = SecKeyCopyPublicKey(priv);
|
||||
TEST_ASSERT_NOT_NULL(pub);
|
||||
if (!pub) {
|
||||
CFRelease(priv);
|
||||
CFRelease(attrs);
|
||||
return;
|
||||
}
|
||||
|
||||
CFDataRef ext = SecKeyCopyExternalRepresentation(pub, &error);
|
||||
TEST_ASSERT_NOT_NULL(ext);
|
||||
if (!ext) {
|
||||
CFRelease(pub);
|
||||
CFRelease(priv);
|
||||
CFRelease(attrs);
|
||||
return;
|
||||
}
|
||||
|
||||
CFIndex key_len = CFDataGetLength(ext);
|
||||
std::vector<u8> der(key_len);
|
||||
CFDataGetBytes(ext, CFRangeMake(0, key_len), der.data());
|
||||
CFRelease(ext);
|
||||
|
||||
// 2048-bit RSA PKCS#1 RSAPublicKey: 4-byte ASN.1 header + 266-byte body.
|
||||
TEST_ASSERT_EQUAL_INT(270, key_len);
|
||||
TEST_ASSERT_EQUAL_UINT8(0x30, der[0]);
|
||||
TEST_ASSERT_EQUAL_UINT8(0x82, der[1]);
|
||||
|
||||
std::vector<u8> plaintext(48, 0x5a);
|
||||
std::vector<u8> ciphertext(256);
|
||||
TEST_ASSERT_TRUE(xone::auth::rsa_encrypt_pkcs1(span(der), span(plaintext),
|
||||
ciphertext));
|
||||
|
||||
// Decrypt to prove the round-trip.
|
||||
CFDataRef ct = CFDataCreate(kCFAllocatorDefault, ciphertext.data(),
|
||||
static_cast<CFIndex>(ciphertext.size()));
|
||||
CFDataRef dec = SecKeyCreateDecryptedData(
|
||||
priv, kSecKeyAlgorithmRSAEncryptionPKCS1, ct, &error);
|
||||
CFRelease(ct);
|
||||
TEST_ASSERT_NOT_NULL(dec);
|
||||
if (dec) {
|
||||
CFIndex dec_len = CFDataGetLength(dec);
|
||||
TEST_ASSERT_EQUAL_INT(48, dec_len);
|
||||
std::vector<u8> dec_bytes(dec_len);
|
||||
CFDataGetBytes(dec, CFRangeMake(0, dec_len), dec_bytes.data());
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(plaintext.data(), dec_bytes.data(), 48);
|
||||
CFRelease(dec);
|
||||
}
|
||||
|
||||
CFRelease(priv);
|
||||
CFRelease(pub);
|
||||
CFRelease(attrs);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_sha256_abc);
|
||||
RUN_TEST(test_sha256_snapshot);
|
||||
RUN_TEST(test_hmac_rfc4231_case2);
|
||||
RUN_TEST(test_hmac_rfc4231_case1);
|
||||
RUN_TEST(test_prf_expansion);
|
||||
RUN_TEST(test_ec_small_multiples);
|
||||
RUN_TEST(test_ec_base_multiply);
|
||||
RUN_TEST(test_ec_shared_secret);
|
||||
RUN_TEST(test_ec_rejects_off_curve_point);
|
||||
RUN_TEST(test_rsa_roundtrip);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
#include "unity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "gip/protocol.hpp"
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
namespace {
|
||||
|
||||
using u8 = std::uint8_t;
|
||||
using u16 = std::uint16_t;
|
||||
using u32 = std::uint32_t;
|
||||
|
||||
class mock_transport : public xone::gip::transport {
|
||||
public:
|
||||
std::vector<std::vector<u8>> frames;
|
||||
|
||||
auto send_frame(std::span<u8 const> frame) -> int override
|
||||
{
|
||||
frames.emplace_back(frame.begin(), frame.end());
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
class mock_listener : public xone::gip::client_listener {
|
||||
public:
|
||||
int added = 0;
|
||||
int removed = 0;
|
||||
std::vector<std::vector<u8>> inputs;
|
||||
|
||||
auto on_client_added(xone::gip::client&) -> void override { ++added; }
|
||||
auto on_client_removed(u8) -> void override { ++removed; }
|
||||
auto on_input(xone::gip::client&, std::span<u8 const> data) -> void override
|
||||
{
|
||||
inputs.emplace_back(data.begin(), data.end());
|
||||
}
|
||||
};
|
||||
|
||||
// Build a controller -> host GIP frame with a simple (single-byte varint)
|
||||
// header. Payloads here stay below 128 bytes so the header is even without
|
||||
// the pad byte.
|
||||
auto make_frame(u8 command, u8 options, u8 sequence,
|
||||
std::vector<u8> const& payload) -> std::vector<u8>
|
||||
{
|
||||
std::vector<u8> f;
|
||||
f.push_back(command);
|
||||
f.push_back(options);
|
||||
f.push_back(sequence);
|
||||
u32 len = static_cast<u32>(payload.size());
|
||||
do {
|
||||
u8 b = static_cast<u8>(len & 0x7f);
|
||||
len >>= 7;
|
||||
if (len)
|
||||
b |= 0x80;
|
||||
f.push_back(b);
|
||||
} while (len);
|
||||
f.insert(f.end(), payload.begin(), payload.end());
|
||||
return f;
|
||||
}
|
||||
|
||||
auto make_announce() -> std::vector<u8>
|
||||
{
|
||||
std::vector<u8> p(28, 0);
|
||||
p[8] = 0x5e; // vendor 0x045e (LE)
|
||||
p[9] = 0x04;
|
||||
p[10] = 0x12; // product 0x0b12
|
||||
p[11] = 0x0b;
|
||||
p[12] = 0x05; // fw major
|
||||
p[14] = 0x0f; // fw minor
|
||||
return p;
|
||||
}
|
||||
|
||||
auto make_identify() -> std::vector<u8>
|
||||
{
|
||||
// identify payload = 16 unknown bytes + 8 LE16 offsets + info elements.
|
||||
// The offsets are relative to the base right after the unknown header
|
||||
// (so they include the 16-byte offsets array itself: first element at 16).
|
||||
auto put16 = [](std::vector<u8>& v, u16 x) {
|
||||
v.push_back(static_cast<u8>(x & 0xFF));
|
||||
v.push_back(static_cast<u8>(x >> 8));
|
||||
};
|
||||
|
||||
std::vector<u8> elems;
|
||||
// classes at info offset 16: count=1, str_len=10, "controller"
|
||||
elems.push_back(1);
|
||||
put16(elems, 10);
|
||||
for (char c : std::string("controller"))
|
||||
elems.push_back(static_cast<u8>(c));
|
||||
// firmware_versions at 29: count=1, {5, 15}
|
||||
elems.push_back(1);
|
||||
elems.push_back(5);
|
||||
elems.push_back(0);
|
||||
elems.push_back(15);
|
||||
elems.push_back(0);
|
||||
// capabilities_out at 34: count=1, 0x0f
|
||||
elems.push_back(1);
|
||||
elems.push_back(0x0f);
|
||||
// capabilities_in at 36: count=1, 0x0f
|
||||
elems.push_back(1);
|
||||
elems.push_back(0x0f);
|
||||
// interfaces at 38: count=1, 16-byte guid
|
||||
elems.push_back(1);
|
||||
for (int i = 0; i < 16; ++i)
|
||||
elems.push_back(static_cast<u8>(i));
|
||||
|
||||
u16 offsets[8] = {0, 29, 0, 34, 36, 16, 38, 0};
|
||||
|
||||
std::vector<u8> payload(16, 0); // unknown header
|
||||
for (u16 off : offsets)
|
||||
put16(payload, off);
|
||||
payload.insert(payload.end(), elems.begin(), elems.end());
|
||||
return payload;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void test_announce_requests_identification(void)
|
||||
{
|
||||
mock_transport transport;
|
||||
mock_listener listener;
|
||||
xone::gip::adapter adap{transport, listener};
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, adap.process_buffer(make_frame(0x02, 0x21, 1, make_announce())));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, transport.frames.size());
|
||||
auto const& f = transport.frames[0];
|
||||
TEST_ASSERT_EQUAL_INT(4, f.size()); // command + options + seq + len
|
||||
TEST_ASSERT_EQUAL_UINT8(0x04, f[0]); // identify
|
||||
TEST_ASSERT_EQUAL_UINT8(0x21, f[1]); // id 1 | internal
|
||||
TEST_ASSERT_EQUAL_UINT8(0x01, f[2]); // sequence
|
||||
TEST_ASSERT_EQUAL_UINT8(0x00, f[3]); // zero packet length
|
||||
}
|
||||
|
||||
void test_identify_adds_client_and_starts_auth(void)
|
||||
{
|
||||
mock_transport transport;
|
||||
mock_listener listener;
|
||||
xone::gip::adapter adap{transport, listener};
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, adap.process_buffer(make_frame(0x04, 0x21, 2, make_identify())));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, listener.added);
|
||||
TEST_ASSERT_EQUAL_INT(1, adap.client_count());
|
||||
|
||||
// auth started: one AUTHENTICATE host hello frame was sent
|
||||
TEST_ASSERT_EQUAL_INT(1, transport.frames.size());
|
||||
auto const& f = transport.frames[0];
|
||||
TEST_ASSERT_EQUAL_UINT8(0x06, f[0]); // authenticate
|
||||
TEST_ASSERT_EQUAL_UINT8(0x31, f[1]); // id 1 | internal | ack
|
||||
TEST_ASSERT_EQUAL_INT(62, f.size()); // 4-byte header + 58-byte hello
|
||||
}
|
||||
|
||||
void test_input_delivers_to_listener(void)
|
||||
{
|
||||
mock_transport transport;
|
||||
mock_listener listener;
|
||||
xone::gip::adapter adap{transport, listener};
|
||||
|
||||
std::vector<u8> payload = {0x11, 0x22, 0x33, 0x44};
|
||||
TEST_ASSERT_EQUAL_INT(0, adap.process_buffer(make_frame(0x20, 0x01, 3, payload)));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, listener.inputs.size());
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(payload.data(), listener.inputs[0].data(), 4);
|
||||
}
|
||||
|
||||
void test_status_disconnect_removes_client(void)
|
||||
{
|
||||
mock_transport transport;
|
||||
mock_listener listener;
|
||||
xone::gip::adapter adap{transport, listener};
|
||||
|
||||
adap.get_client(1);
|
||||
TEST_ASSERT_EQUAL_INT(1, adap.client_count());
|
||||
|
||||
std::vector<u8> payload = {0x00, 0, 0, 0}; // status, not connected
|
||||
TEST_ASSERT_EQUAL_INT(0, adap.process_buffer(make_frame(0x03, 0x21, 4, payload)));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, listener.removed);
|
||||
TEST_ASSERT_EQUAL_INT(0, adap.client_count());
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_announce_requests_identification);
|
||||
RUN_TEST(test_identify_adds_client_and_starts_auth);
|
||||
RUN_TEST(test_input_delivers_to_listener);
|
||||
RUN_TEST(test_status_disconnect_removes_client);
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user