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:
portersky
2026-08-17 15:04:44 +02:00
parent 95e958a7e8
commit 2d4366f454
13 changed files with 3217 additions and 14 deletions
+92
View File
@@ -0,0 +1,92 @@
#pragma once
// ==============================================================================
// GIP authentication handshake
// ==============================================================================
// Port target: medusalix/xone auth/auth.c + auth/auth.h.
//
// The kernel driver runs the RSA/ECDH exchanges on workqueues and speaks to
// the controller through gip_send_authenticate(). Here the exchanges run
// synchronously and the GIP layer is reached through the auth_sink callback
// interface, which the gip::client implements.
// ==============================================================================
#include <array>
#include <cstdint>
#include <span>
#include "auth/crypto.hpp"
namespace xone::auth {
using u8 = std::uint8_t;
using u16 = std::uint16_t;
// Wire sizes from the upstream auth.h.
inline constexpr auto k_trailer_len = 8;
inline constexpr auto k_random_len = 32;
inline constexpr auto k_certificate_max_len = 1024;
inline constexpr auto k_pubkey_len = 270; // v1 RSA public key
inline constexpr auto k_secret_len = 48;
inline constexpr auto k_encrypted_pms_len = 256;
inline constexpr auto k_transcript_len = 32;
inline constexpr auto k_session_key_len = 16;
inline constexpr auto k_pubkey2_len = 64; // v2 EC public key (X || Y)
inline constexpr auto k_secret2_len = 32;
// Callback interface implemented by the GIP layer.
class auth_sink {
public:
virtual ~auth_sink() = default;
// Send an AUTHENTICATE command packet to the controller.
virtual auto send(std::span<u8 const> pkt, bool acknowledge) -> int = 0;
// Install the derived AES-CCMP session key (16 bytes).
virtual auto set_encryption_key(std::span<u8 const> key) -> int = 0;
};
// Runs the handshake for one controller. Port of struct gip_auth.
class auth {
public:
explicit auth(auth_sink& sink);
// Begin the handshake by sending the (v1) host hello.
auto start() -> int;
// Process one incoming AUTHENTICATE packet from the controller.
auto process_pkt(std::span<u8 const> data) -> int;
private:
auto send_pkt(u8 cmd, void const* pkt, std::size_t len) -> int;
auto request_pkt(u8 cmd, std::uint16_t len) -> int;
auto send_hello() -> int;
auto send_hello2() -> int;
auto send_finish(u8 cmd) -> int;
auto send_complete() -> int;
auto exchange_rsa() -> void;
auto exchange_ecdh() -> void;
auto complete_handshake() -> void;
auto handle_pkt_acknowledge() -> int;
auto handle_pkt_data(std::span<u8 const> data) -> int;
auto dispatch_pkt(u8 cmd, std::span<u8 const> data) -> int;
auto handle_hello(std::span<u8 const> data) -> int;
auto handle_certificate(std::span<u8 const> data) -> int;
auto handle_finish(std::span<u8 const> data) -> int;
auto handle_hello2(std::span<u8 const> data) -> int;
auto handle_certificate2(std::span<u8 const> data) -> int;
auto handle_pubkey(std::span<u8 const> data) -> int;
auto get_transcript() -> std::array<u8, k_transcript_len>;
auth_sink& sink_;
sha256 transcript_;
u8 last_sent_command_ = 0;
std::array<u8, k_random_len> random_host_{};
std::array<u8, k_random_len> random_client_{};
std::array<u8, k_pubkey_len> pubkey_client_{};
std::array<u8, k_pubkey2_len> pubkey_client2_{};
std::array<u8, k_secret_len> master_secret_{};
};
} // namespace xone::auth
+100 -6
View File
@@ -1,16 +1,110 @@
#pragma once
// ==============================================================================
// Auth + crypto
// Auth + crypto primitives
// ==============================================================================
// Port target: medusalix/xone auth/auth.c + auth/crypto.c.
// ECDH (P-256) key agreement, RSA encryption, SHA transcript/PRF for the GIP
// authentication handshake. AES-CCMP frame encryption itself runs on-chip;
// this layer only derives and installs keys into the MT76 WCID registers.
// Port target: medusalix/xone auth/crypto.c + auth/crypto.h.
//
// The kernel driver uses the in-kernel crypto API (crypto_shash for
// SHA-256/HMAC, crypto_akcipher for PKCS#1 RSA, crypto_kpp for ECDH
// P-256). These are replaced here with user-space equivalents:
// - SHA-256 / HMAC-SHA256: CommonCrypto (part of libSystem)
// - RSA PKCS#1 v1.5: Security.framework
// - ECDH P-256: self-contained implementation (no public
// macOS C API exists for EC key agreement)
// - randomness: arc4random_buf
//
// AES-CCMP frame encryption itself runs on the MT76 chip; this layer
// only derives keys and installs them (see xone_mt76_set_client_key).
// ==============================================================================
#include <array>
#include <cstdint>
#include <span>
#include <string_view>
#include <CommonCrypto/CommonDigest.h>
namespace xone::auth {
// TODO(phase 1): ECDH/RSA/SHA primitives (CommonCrypto / Security.framework).
using u8 = std::uint8_t;
inline constexpr auto k_sha256_len = 32;
// --------------------------------------------------------------------------
// SHA-256
// --------------------------------------------------------------------------
// Incremental SHA-256. Instances are cheap to copy; a copy is an
// independent snapshot of the current hash state, which the handshake
// uses to compute the running transcript without losing progress.
class sha256 {
public:
sha256();
~sha256() = default;
sha256(sha256 const&) = default;
auto operator=(sha256 const&) -> sha256& = default;
auto update(std::span<u8 const> data) -> void;
auto finalize(std::array<u8, k_sha256_len>& out) -> void;
private:
CC_SHA256_CTX ctx_;
};
// --------------------------------------------------------------------------
// HMAC-SHA256
// --------------------------------------------------------------------------
class hmac_sha256 {
public:
explicit hmac_sha256(std::span<u8 const> key);
auto update(std::span<u8 const> data) -> void;
auto finalize(std::array<u8, k_sha256_len>& out) -> void;
private:
sha256 inner_;
sha256 outer_;
};
// --------------------------------------------------------------------------
// TLS P_SHA256 PRF
// --------------------------------------------------------------------------
// Expands key + label + seed into out_len bytes, exactly like the kernel
// driver's gip_auth_compute_prf(). Used to derive the master secret, the
// handshake transcript checks, and the session key.
auto prf_sha256(std::span<u8 const> key, std::string_view label,
std::span<u8 const> seed, std::span<u8> out) -> void;
// --------------------------------------------------------------------------
// RSA (PKCS#1 v1.5)
// --------------------------------------------------------------------------
// Encrypts plaintext with a DER RSAPublicKey (the 270-byte ASN.1 SEQUENCE
// that Microsoft controllers embed in their X.509 certificate). out must
// be at least the RSA modulus size (256 bytes for 2048-bit keys).
auto rsa_encrypt_pkcs1(std::span<u8 const> der_key,
std::span<u8 const> plaintext, std::span<u8> out)
-> bool;
// --------------------------------------------------------------------------
// ECDH (P-256)
// --------------------------------------------------------------------------
using ec_scalar = std::array<u8, 32>; // big-endian private key / x-coord
using ec_point = std::array<u8, 64>; // X || Y, big-endian, no 0x04 prefix
// q = d * G.
auto ec_base_point_multiply(ec_scalar const& d, ec_point& q) -> void;
// shared = x-coordinate of d * peer (the standard P-256 shared secret).
// Returns false if peer is not a valid point on the curve.
auto ec_compute_shared(ec_scalar const& d, ec_point const& peer,
ec_scalar& shared) -> bool;
// Generate a random keypair.
auto ec_generate_keypair(ec_scalar& d, ec_point& q) -> void;
// --------------------------------------------------------------------------
// Randomness
// --------------------------------------------------------------------------
auto random_bytes(std::span<u8> out) -> void;
} // namespace xone::auth