23 changed files with 1614 additions and 28 deletions
@ -0,0 +1,616 @@
|
||||
/* reality.c — REALITY-style TLS ClientHello/ServerHello camouflage
|
||||
* |
||||
* Реализует формирование и разбор ClientHello/ServerHello по схеме REALITY |
||||
* (см. 3.go): аутентификация в SessionId ClientHello. |
||||
* |
||||
* открытый текст (16 байт) = version[3] + reserved(1) + timestamp(4, BE) + ShortId[8] |
||||
* SessionId[0:32] = AES-256-GCM(открытый текст) = ciphertext(16) + tag(16) |
||||
* |
||||
* AuthKey = HKDF-SHA256(X25519(eph, static), Random[0:20], "REALITY") |
||||
* nonce = Random[20:32] |
||||
* AAD = весь ClientHello handshake-сообщение с обнулённым SessionId |
||||
*/ |
||||
|
||||
#ifdef HAVE_CONFIG_H |
||||
#include <config.h> |
||||
#endif |
||||
|
||||
#define OPENSSL_API_COMPAT 0x10100000L |
||||
|
||||
#include "reality.h" |
||||
#include "reality_fingerprint.h" |
||||
#include "../lib/debug_config.h" |
||||
#include "../lib/platform_compat.h" |
||||
#include <string.h> |
||||
#include <time.h> |
||||
#include <openssl/evp.h> |
||||
#include <openssl/kdf.h> |
||||
|
||||
// TLS-константы
|
||||
#define TLS_RECORD_HANDSHAKE 0x16 |
||||
#define TLS_HANDSHAKE_CLIENT_HELLO 0x01 |
||||
#define TLS_HANDSHAKE_SERVER_HELLO 0x02 |
||||
#define TLS_EXT_SERVER_NAME 0x0000 |
||||
#define TLS_EXT_SUPPORTED_GROUPS 0x000a |
||||
#define TLS_EXT_SIG_ALGS 0x000d |
||||
#define TLS_EXT_ALPN 0x0010 |
||||
#define TLS_EXT_SUPPORTED_VERSIONS 0x002b |
||||
#define TLS_EXT_PSK_KEY_EXCHANGE 0x002d |
||||
#define TLS_EXT_KEY_SHARE 0x0033 |
||||
#define TLS_GROUP_X25519 0x001d |
||||
#define TLS_VERSION_1_3 0x0304 |
||||
#define TLS_CIPHER_AES_128_GCM 0x1301 |
||||
|
||||
#define REALITY_HKDF_INFO "REALITY" |
||||
|
||||
// ─── Буфер записи с контролем границ ───
|
||||
struct rbuf { |
||||
uint8_t *p; |
||||
size_t cap; |
||||
size_t len; |
||||
}; |
||||
|
||||
static int rbuf_put(struct rbuf *b, const void *data, size_t n) { |
||||
if (b->len + n > b->cap) return -1; |
||||
memcpy(b->p + b->len, data, n); |
||||
b->len += n; |
||||
return 0; |
||||
} |
||||
static int rbuf_put_u8(struct rbuf *b, uint8_t v) { return rbuf_put(b, &v, 1); } |
||||
static int rbuf_put_u16(struct rbuf *b, uint16_t v) { |
||||
uint8_t t[2] = { (uint8_t)(v >> 8), (uint8_t)(v & 0xff) }; |
||||
return rbuf_put(b, t, 2); |
||||
} |
||||
// Вписывает 2-байтную длину по уже известной позиции (для extension-блоков).
|
||||
static void rbuf_patch_u16(struct rbuf *b, size_t pos, uint16_t v) { |
||||
if (pos + 2 <= b->cap) { |
||||
b->p[pos] = (uint8_t)(v >> 8); |
||||
b->p[pos + 1] = (uint8_t)(v & 0xff); |
||||
} |
||||
} |
||||
|
||||
// ─── Крипто-хелперы (OpenSSL) ───
|
||||
|
||||
static int reality_gen_keypair(uint8_t priv[32], uint8_t pub[32]) { |
||||
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL); |
||||
if (!ctx) return -1; |
||||
EVP_PKEY *pkey = NULL; |
||||
int rc = -1; |
||||
if (EVP_PKEY_keygen_init(ctx) <= 0) goto out; |
||||
if (EVP_PKEY_keygen(ctx, &pkey) <= 0) goto out; |
||||
size_t plen = 32, qlen = 32; |
||||
if (EVP_PKEY_get_raw_private_key(pkey, priv, &plen) <= 0 || plen != 32) goto out; |
||||
if (EVP_PKEY_get_raw_public_key(pkey, pub, &qlen) <= 0 || qlen != 32) goto out; |
||||
rc = 0; |
||||
out: |
||||
EVP_PKEY_free(pkey); |
||||
EVP_PKEY_CTX_free(ctx); |
||||
return rc; |
||||
} |
||||
|
||||
int reality_generate_keypair(uint8_t priv[32], uint8_t pub[32]) { |
||||
return reality_gen_keypair(priv, pub) == 0 ? REALITY_OK : REALITY_ERR_CRYPTO; |
||||
} |
||||
|
||||
int reality_pubkey_from_priv(const uint8_t priv[32], uint8_t pub[32]) { |
||||
if (!priv || !pub) return REALITY_ERR_ARG; |
||||
EVP_PKEY *pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, priv, 32); |
||||
if (!pkey) return REALITY_ERR_CRYPTO; |
||||
size_t len = 32; |
||||
int rc = (EVP_PKEY_get_raw_public_key(pkey, pub, &len) <= 0 || len != 32) ? REALITY_ERR_CRYPTO : REALITY_OK; |
||||
EVP_PKEY_free(pkey); |
||||
return rc; |
||||
} |
||||
|
||||
void reality_config_set_defaults(struct reality_config *cfg) { |
||||
if (!cfg) return; |
||||
cfg->enabled = 0; |
||||
cfg->server_name[0] = '\0'; |
||||
cfg->dest[0] = '\0'; |
||||
memset(cfg->short_id, 0, sizeof(cfg->short_id)); |
||||
memset(cfg->short_ids, 0, sizeof(cfg->short_ids)); |
||||
cfg->short_id_count = 0; |
||||
memset(cfg->public_key, 0, sizeof(cfg->public_key)); |
||||
memset(cfg->private_key, 0, sizeof(cfg->private_key)); |
||||
cfg->has_public_key = 0; |
||||
cfg->has_private_key = 0; |
||||
cfg->version[0] = 1; cfg->version[1] = 0; cfg->version[2] = 0; |
||||
cfg->time_window_sec = 30; |
||||
cfg->fingerprint = REALITY_FP_CHROME; |
||||
} |
||||
|
||||
static int reality_x25519(const uint8_t priv[32], const uint8_t pub[32], uint8_t shared[32]) { |
||||
EVP_PKEY *pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, priv, 32); |
||||
if (!pkey) return -1; |
||||
EVP_PKEY *peer = EVP_PKEY_new_raw_public_key(EVP_PKEY_X25519, NULL, pub, 32); |
||||
if (!peer) { EVP_PKEY_free(pkey); return -1; } |
||||
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new(pkey, NULL); |
||||
if (!ctx) { EVP_PKEY_free(pkey); EVP_PKEY_free(peer); return -1; } |
||||
int rc = -1; |
||||
if (EVP_PKEY_derive_init(ctx) <= 0) goto out; |
||||
if (EVP_PKEY_derive_set_peer(ctx, peer) <= 0) goto out; |
||||
size_t len = 32; |
||||
if (EVP_PKEY_derive(ctx, shared, &len) <= 0 || len != 32) goto out; |
||||
rc = 0; |
||||
out: |
||||
EVP_PKEY_CTX_free(ctx); |
||||
EVP_PKEY_free(pkey); |
||||
EVP_PKEY_free(peer); |
||||
return rc; |
||||
} |
||||
|
||||
static int reality_hkdf_sha256(const uint8_t *ikm, size_t ikm_len, |
||||
const uint8_t *salt, size_t salt_len, |
||||
const uint8_t *info, size_t info_len, |
||||
uint8_t *out, size_t out_len) { |
||||
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL); |
||||
if (!ctx) return -1; |
||||
int rc = -1; |
||||
if (EVP_PKEY_derive_init(ctx) <= 0) goto out; |
||||
if (EVP_PKEY_CTX_set_hkdf_md(ctx, EVP_sha256()) <= 0) goto out; |
||||
if (EVP_PKEY_CTX_set1_hkdf_salt(ctx, salt, (int)salt_len) <= 0) goto out; |
||||
if (EVP_PKEY_CTX_set1_hkdf_key(ctx, ikm, (int)ikm_len) <= 0) goto out; |
||||
if (EVP_PKEY_CTX_add1_hkdf_info(ctx, info, (int)info_len) <= 0) goto out; |
||||
size_t len = out_len; |
||||
if (EVP_PKEY_derive(ctx, out, &len) <= 0 || len != out_len) goto out; |
||||
rc = 0; |
||||
out: |
||||
EVP_PKEY_CTX_free(ctx); |
||||
return rc; |
||||
} |
||||
|
||||
static int reality_aes_gcm_seal(const uint8_t key[32], const uint8_t nonce[12], |
||||
const uint8_t *aad, size_t aad_len, |
||||
const uint8_t *plain, size_t plain_len, |
||||
uint8_t *cipher, uint8_t tag[16]) { |
||||
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); |
||||
if (!ctx) return -1; |
||||
int rc = -1, len = 0, tmplen = 0; |
||||
if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1) goto out; |
||||
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, NULL) != 1) goto out; |
||||
if (EVP_EncryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) goto out; |
||||
if (aad_len && EVP_EncryptUpdate(ctx, NULL, &len, aad, (int)aad_len) != 1) goto out; |
||||
if (EVP_EncryptUpdate(ctx, cipher, &len, plain, (int)plain_len) != 1) goto out; |
||||
if (EVP_EncryptFinal_ex(ctx, cipher + len, &tmplen) != 1) goto out; |
||||
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, 16, tag) != 1) goto out; |
||||
rc = 0; |
||||
out: |
||||
EVP_CIPHER_CTX_free(ctx); |
||||
return rc; |
||||
} |
||||
|
||||
static int reality_aes_gcm_open(const uint8_t key[32], const uint8_t nonce[12], |
||||
const uint8_t *aad, size_t aad_len, |
||||
const uint8_t *cipher, size_t cipher_len, |
||||
const uint8_t tag[16], uint8_t *plain) { |
||||
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); |
||||
if (!ctx) return -1; |
||||
int rc = -1, len = 0, tmplen = 0; |
||||
if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1) goto out; |
||||
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, NULL) != 1) goto out; |
||||
if (EVP_DecryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) goto out; |
||||
if (aad_len && EVP_DecryptUpdate(ctx, NULL, &len, aad, (int)aad_len) != 1) goto out; |
||||
if (EVP_DecryptUpdate(ctx, plain, &len, cipher, (int)cipher_len) != 1) goto out; |
||||
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, 16, (void *)tag) != 1) goto out; |
||||
if (EVP_DecryptFinal_ex(ctx, plain + len, &tmplen) != 1) goto out; // проверка тега
|
||||
rc = 0; |
||||
out: |
||||
EVP_CIPHER_CTX_free(ctx); |
||||
return rc; |
||||
} |
||||
|
||||
// Сборка ClientHello body (после 4-байтного handshake-заголовка).
|
||||
// SessionId пишется нулями (32 байта) — далее по нему считается AAD.
|
||||
static int reality_build_client_hello_body(const struct reality_client_config *cfg, |
||||
const struct reality_fingerprint *fp, |
||||
const uint8_t *random32, |
||||
const uint8_t *eph_pub, |
||||
uint8_t *body, size_t body_cap, size_t *body_len) { |
||||
struct rbuf b = { body, body_cap, 0 }; |
||||
if (rbuf_put_u16(&b, 0x0303) < 0) return REALITY_ERR_ARG; // legacy_version
|
||||
if (rbuf_put(&b, random32, 32) < 0) return REALITY_ERR_ARG; // random
|
||||
if (rbuf_put_u8(&b, 32) < 0) return REALITY_ERR_ARG; // session id len
|
||||
uint8_t zeros[32] = {0}; |
||||
if (rbuf_put(&b, zeros, 32) < 0) return REALITY_ERR_ARG; // session id (нули)
|
||||
|
||||
// cipher suites
|
||||
int cs_count = 0; |
||||
for (const uint16_t *cs = fp->cipher_suites; *cs != 0; cs++) cs_count++; |
||||
if (rbuf_put_u16(&b, (uint16_t)(cs_count * 2)) < 0) return REALITY_ERR_ARG; |
||||
for (const uint16_t *cs = fp->cipher_suites; *cs != 0; cs++) |
||||
if (rbuf_put_u16(&b, *cs) < 0) return REALITY_ERR_ARG; |
||||
|
||||
// compression
|
||||
if (rbuf_put_u8(&b, 1) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u8(&b, 0) < 0) return REALITY_ERR_ARG; |
||||
|
||||
// extensions
|
||||
size_t ext_len_pos = b.len; |
||||
if (rbuf_put_u16(&b, 0) < 0) return REALITY_ERR_ARG; // placeholder
|
||||
|
||||
// server_name (SNI)
|
||||
size_t name_len = strlen(cfg->server_name); |
||||
{ |
||||
uint16_t ext_data_len = (uint16_t)(5 + name_len); |
||||
if (rbuf_put_u16(&b, TLS_EXT_SERVER_NAME) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u16(&b, ext_data_len) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u16(&b, (uint16_t)(3 + name_len)) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u8(&b, 0) < 0) return REALITY_ERR_ARG; // name_type = host_name
|
||||
if (rbuf_put_u16(&b, (uint16_t)name_len) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put(&b, cfg->server_name, name_len) < 0) return REALITY_ERR_ARG; |
||||
} |
||||
|
||||
// supported_groups
|
||||
{ |
||||
int count = 0; |
||||
for (const uint16_t *g = fp->supported_groups; *g != 0; g++) count++; |
||||
if (rbuf_put_u16(&b, TLS_EXT_SUPPORTED_GROUPS) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u16(&b, (uint16_t)(count * 2)) < 0) return REALITY_ERR_ARG; |
||||
for (const uint16_t *g = fp->supported_groups; *g != 0; g++) |
||||
if (rbuf_put_u16(&b, *g) < 0) return REALITY_ERR_ARG; |
||||
} |
||||
|
||||
// signature_algorithms
|
||||
{ |
||||
int count = 0; |
||||
for (const uint16_t *s = fp->sig_algs; *s != 0; s++) count++; |
||||
if (rbuf_put_u16(&b, TLS_EXT_SIG_ALGS) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u16(&b, (uint16_t)(count * 2)) < 0) return REALITY_ERR_ARG; |
||||
for (const uint16_t *s = fp->sig_algs; *s != 0; s++) |
||||
if (rbuf_put_u16(&b, *s) < 0) return REALITY_ERR_ARG; |
||||
} |
||||
|
||||
// supported_versions
|
||||
if (rbuf_put_u16(&b, TLS_EXT_SUPPORTED_VERSIONS) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u16(&b, 3) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u8(&b, 2) < 0) return REALITY_ERR_ARG; // длина списка
|
||||
if (rbuf_put_u16(&b, TLS_VERSION_1_3) < 0) return REALITY_ERR_ARG; |
||||
|
||||
// psk_key_exchange_modes
|
||||
if (rbuf_put_u16(&b, TLS_EXT_PSK_KEY_EXCHANGE) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u16(&b, 2) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u8(&b, 1) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u8(&b, 1) < 0) return REALITY_ERR_ARG; // psk_dhe_ke
|
||||
|
||||
// key_share (X25519)
|
||||
if (rbuf_put_u16(&b, TLS_EXT_KEY_SHARE) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u16(&b, 38) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u16(&b, 36) < 0) return REALITY_ERR_ARG; // client_shares_len
|
||||
if (rbuf_put_u16(&b, TLS_GROUP_X25519) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u16(&b, 32) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put(&b, eph_pub, 32) < 0) return REALITY_ERR_ARG; |
||||
|
||||
// ALPN
|
||||
{ |
||||
size_t alpn_list_len = 0; |
||||
for (const char *const *a = fp->alpn; *a; a++) alpn_list_len += 1 + strlen(*a); |
||||
if (rbuf_put_u16(&b, TLS_EXT_ALPN) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u16(&b, (uint16_t)(alpn_list_len + 2)) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u16(&b, (uint16_t)alpn_list_len) < 0) return REALITY_ERR_ARG; |
||||
for (const char *const *a = fp->alpn; *a; a++) { |
||||
size_t l = strlen(*a); |
||||
if (rbuf_put_u8(&b, (uint8_t)l) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put(&b, *a, l) < 0) return REALITY_ERR_ARG; |
||||
} |
||||
} |
||||
|
||||
rbuf_patch_u16(&b, ext_len_pos, (uint16_t)(b.len - ext_len_pos - 2)); |
||||
*body_len = b.len; |
||||
return REALITY_OK; |
||||
} |
||||
|
||||
int reality_client_hello_build(const struct reality_client_config *cfg, |
||||
uint8_t *out, size_t out_cap, size_t *out_len) { |
||||
return reality_client_hello_build_at(cfg, (uint32_t)time(NULL), out, out_cap, out_len); |
||||
} |
||||
|
||||
int reality_client_hello_build_at(const struct reality_client_config *cfg, uint32_t now, |
||||
uint8_t *out, size_t out_cap, size_t *out_len) { |
||||
if (!cfg || !out || !out_len || out_cap < REALITY_MAX_CH_SIZE) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: invalid args"); |
||||
return REALITY_ERR_ARG; |
||||
} |
||||
const struct reality_fingerprint *fp = reality_fingerprint_get(cfg->fingerprint); |
||||
if (!fp) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: unknown fingerprint %d", cfg->fingerprint); |
||||
return REALITY_ERR_ARG; |
||||
} |
||||
if (cfg->server_name[0] == '\0') { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: empty server_name"); |
||||
return REALITY_ERR_ARG; |
||||
} |
||||
|
||||
// 1. эфемерный ключ + random
|
||||
uint8_t eph_priv[32], eph_pub[32], random32[32]; |
||||
if (reality_gen_keypair(eph_priv, eph_pub) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: X25519 keygen failed"); |
||||
return REALITY_ERR_CRYPTO; |
||||
} |
||||
if (random_bytes(random32, sizeof(random32)) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: random_bytes failed"); |
||||
return REALITY_ERR_CRYPTO; |
||||
} |
||||
|
||||
// 2. shared + AuthKey
|
||||
uint8_t shared[32], auth_key[32]; |
||||
if (reality_x25519(eph_priv, cfg->server_static_pubkey, shared) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: X25519 derive failed"); |
||||
return REALITY_ERR_CRYPTO; |
||||
} |
||||
if (reality_hkdf_sha256(shared, sizeof(shared), random32, 20, |
||||
(const uint8_t *)REALITY_HKDF_INFO, sizeof(REALITY_HKDF_INFO) - 1, |
||||
auth_key, sizeof(auth_key)) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: HKDF failed"); |
||||
return REALITY_ERR_CRYPTO; |
||||
} |
||||
|
||||
// 3. plaintext[0:16] = version + reserved + timestamp + short_id
|
||||
uint8_t plaintext[16]; |
||||
memcpy(plaintext, cfg->version, 3); |
||||
plaintext[3] = 0; |
||||
plaintext[4] = (uint8_t)(now >> 24); |
||||
plaintext[5] = (uint8_t)(now >> 16); |
||||
plaintext[6] = (uint8_t)(now >> 8); |
||||
plaintext[7] = (uint8_t)(now); |
||||
memcpy(plaintext + 8, cfg->short_id, REALITY_SHORT_ID_SIZE); |
||||
|
||||
// 4. собрать body (SessionId = нули)
|
||||
uint8_t body[REALITY_MAX_CH_SIZE]; |
||||
size_t body_len = 0; |
||||
int rc = reality_build_client_hello_body(cfg, fp, random32, eph_pub, body, sizeof(body), &body_len); |
||||
if (rc != REALITY_OK) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: body build failed rc=%d", rc); |
||||
return rc; |
||||
} |
||||
|
||||
// 5. handshake-сообщение = 0x01 + length + body (SessionId на позиции 39..71)
|
||||
uint8_t hs[REALITY_MAX_CH_SIZE]; |
||||
size_t hs_len = 4 + body_len; |
||||
hs[0] = TLS_HANDSHAKE_CLIENT_HELLO; |
||||
hs[1] = (uint8_t)(body_len >> 16); |
||||
hs[2] = (uint8_t)(body_len >> 8); |
||||
hs[3] = (uint8_t)(body_len); |
||||
memcpy(hs + 4, body, body_len); |
||||
|
||||
// 6. AAD = hs с обнулённым SessionId (уже нули), seal → ciphertext(16)+tag(16)=32 байта
|
||||
uint8_t seal_out[32]; |
||||
if (reality_aes_gcm_seal(auth_key, random32 + 20, hs, hs_len, |
||||
plaintext, 16, seal_out, seal_out + 16) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_client_hello_build: AES-GCM seal failed"); |
||||
return REALITY_ERR_CRYPTO; |
||||
} |
||||
memcpy(hs + 39, seal_out, 32); // SessionId = ciphertext || tag
|
||||
|
||||
// 7. TLS record = 0x16 0x0301 length + hs
|
||||
size_t total = 5 + hs_len; |
||||
out[0] = TLS_RECORD_HANDSHAKE; |
||||
out[1] = 0x03; |
||||
out[2] = 0x01; |
||||
out[3] = (uint8_t)(hs_len >> 8); |
||||
out[4] = (uint8_t)(hs_len); |
||||
memcpy(out + 5, hs, hs_len); |
||||
*out_len = total; |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_REALITY, |
||||
"client hello built: total=%zu sn=%s ver=%d.%d.%d short_id=%02x%02x%02x%02x%02x%02x%02x%02x", |
||||
total, cfg->server_name, cfg->version[0], cfg->version[1], cfg->version[2], |
||||
plaintext[8], plaintext[9], plaintext[10], plaintext[11], |
||||
plaintext[12], plaintext[13], plaintext[14], plaintext[15]); |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_REALITY, "client auth_key=%02x%02x%02x%02x... shared=%02x%02x%02x%02x...", |
||||
auth_key[0], auth_key[1], auth_key[2], auth_key[3], |
||||
shared[0], shared[1], shared[2], shared[3]); |
||||
return REALITY_OK; |
||||
} |
||||
|
||||
// ─── Сервер: разбор ClientHello ───
|
||||
|
||||
struct reality_ch_parsed { |
||||
const uint8_t *random; // 32 байта
|
||||
const uint8_t *session_id; // 32 байта
|
||||
const uint8_t *key_share; // 32 байта X25519 ключ (NULL, если нет)
|
||||
}; |
||||
|
||||
// Возвращает REALITY_OK / REALITY_ERR_FORMAT.
|
||||
static int reality_parse_client_hello(const uint8_t *ch, size_t ch_len, |
||||
const uint8_t **hs_out, size_t *hs_len_out, |
||||
struct reality_ch_parsed *p) { |
||||
if (!ch || ch_len < 5 || ch[0] != TLS_RECORD_HANDSHAKE) return REALITY_ERR_FORMAT; |
||||
size_t rec_len = ((size_t)ch[3] << 8) | ch[4]; |
||||
if (ch_len < 5 + rec_len) return REALITY_ERR_FORMAT; |
||||
|
||||
const uint8_t *hs = ch + 5; |
||||
size_t hs_len = rec_len; |
||||
if (hs_len < 4 || hs[0] != TLS_HANDSHAKE_CLIENT_HELLO) return REALITY_ERR_FORMAT; |
||||
size_t body_len = ((size_t)hs[1] << 16) | ((size_t)hs[2] << 8) | hs[3]; |
||||
if (hs_len < 4 + body_len) return REALITY_ERR_FORMAT; |
||||
|
||||
const uint8_t *b = hs + 4; |
||||
size_t bl = body_len; |
||||
if (bl < 2 + 32 + 1) return REALITY_ERR_FORMAT; |
||||
b += 2; // legacy_version
|
||||
p->random = b; b += 32; |
||||
uint8_t sid_len = *b; b += 1; |
||||
if (sid_len != 32 || bl < 2 + 32 + 1 + 32) return REALITY_ERR_FORMAT; |
||||
p->session_id = b; b += 32; |
||||
|
||||
// cipher suites
|
||||
if (bl < (size_t)(b - (hs + 4)) + 2) return REALITY_ERR_FORMAT; |
||||
uint16_t cs_len = (uint16_t)((b[0] << 8) | b[1]); b += 2; |
||||
if (bl < (size_t)(b - (hs + 4)) + cs_len + 1) return REALITY_ERR_FORMAT; |
||||
b += cs_len; // cipher suites
|
||||
uint8_t comp_len = *b; b += 1; |
||||
if (bl < (size_t)(b - (hs + 4)) + comp_len + 2) return REALITY_ERR_FORMAT; |
||||
b += comp_len; // compression
|
||||
|
||||
// extensions
|
||||
uint16_t ext_len = (uint16_t)((b[0] << 8) | b[1]); b += 2; |
||||
const uint8_t *ext = b; |
||||
size_t remaining = ext_len; |
||||
const uint8_t *end = ext + remaining; |
||||
if (end > hs + 4 + bl) return REALITY_ERR_FORMAT; |
||||
|
||||
p->key_share = NULL; |
||||
while (remaining >= 4) { |
||||
uint16_t type = (uint16_t)((ext[0] << 8) | ext[1]); |
||||
uint16_t len = (uint16_t)((ext[2] << 8) | ext[3]); |
||||
if (remaining < 4 + len) return REALITY_ERR_FORMAT; |
||||
if (type == TLS_EXT_KEY_SHARE && len >= 4) { |
||||
uint16_t shares_len = (uint16_t)((ext[4] << 8) | ext[5]); |
||||
const uint8_t *s = ext + 6; |
||||
size_t sl = shares_len; |
||||
while (sl >= 4) { |
||||
uint16_t group = (uint16_t)((s[0] << 8) | s[1]); |
||||
uint16_t klen = (uint16_t)((s[2] << 8) | s[3]); |
||||
if (sl < 4 + klen) break; |
||||
if (group == TLS_GROUP_X25519 && klen == 32) { |
||||
p->key_share = s + 4; |
||||
break; |
||||
} |
||||
s += 4 + klen; |
||||
sl -= 4 + klen; |
||||
} |
||||
} |
||||
ext += 4 + len; |
||||
remaining -= 4 + len; |
||||
} |
||||
|
||||
*hs_out = hs; |
||||
*hs_len_out = hs_len; |
||||
return REALITY_OK; |
||||
} |
||||
|
||||
int reality_server_hello_build(const struct reality_server_config *cfg, |
||||
const uint8_t *ch, size_t ch_len, |
||||
uint8_t *out, size_t out_cap, size_t *out_len) { |
||||
if (!cfg || !ch || !out || !out_len || out_cap < REALITY_MAX_SH_SIZE) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: invalid args"); |
||||
return REALITY_ERR_ARG; |
||||
} |
||||
|
||||
struct reality_ch_parsed p; |
||||
const uint8_t *hs; |
||||
size_t hs_len; |
||||
int rc = reality_parse_client_hello(ch, ch_len, &hs, &hs_len, &p); |
||||
if (rc != REALITY_OK) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: parse failed rc=%d", rc); |
||||
return rc; |
||||
} |
||||
if (!p.key_share) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: no X25519 key_share in ClientHello"); |
||||
return REALITY_ERR_AUTH; |
||||
} |
||||
|
||||
// shared + AuthKey
|
||||
uint8_t shared[32], auth_key[32]; |
||||
if (reality_x25519(cfg->static_privkey, p.key_share, shared) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: X25519 derive failed"); |
||||
return REALITY_ERR_CRYPTO; |
||||
} |
||||
if (reality_hkdf_sha256(shared, sizeof(shared), p.random, 20, |
||||
(const uint8_t *)REALITY_HKDF_INFO, sizeof(REALITY_HKDF_INFO) - 1, |
||||
auth_key, sizeof(auth_key)) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: HKDF failed"); |
||||
return REALITY_ERR_CRYPTO; |
||||
} |
||||
|
||||
// AAD = hs с обнулённым SessionId [39:71]
|
||||
uint8_t aad[REALITY_MAX_CH_SIZE]; |
||||
if (hs_len > sizeof(aad)) return REALITY_ERR_FORMAT; |
||||
memcpy(aad, hs, hs_len); |
||||
memset(aad + 39, 0, 32); |
||||
|
||||
// расшифровать SessionId[0:16]=ciphertext, [16:32]=tag
|
||||
uint8_t plain[16]; |
||||
if (reality_aes_gcm_open(auth_key, p.random + 20, aad, hs_len, |
||||
p.session_id, 16, p.session_id + 16, plain) != 0) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: AES-GCM auth failed"); |
||||
return REALITY_ERR_AUTH; |
||||
} |
||||
|
||||
// сверка версии
|
||||
if (memcmp(plain, cfg->version, 3) != 0 || plain[3] != 0) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, |
||||
"reality_server_hello_build: version mismatch got=%d.%d.%d res=%d want=%d.%d.%d", |
||||
plain[0], plain[1], plain[2], plain[3], cfg->version[0], cfg->version[1], cfg->version[2]); |
||||
return REALITY_ERR_AUTH; |
||||
} |
||||
// сверка timestamp
|
||||
uint32_t ts = ((uint32_t)plain[4] << 24) | ((uint32_t)plain[5] << 16) | |
||||
((uint32_t)plain[6] << 8) | plain[7]; |
||||
int64_t now = (int64_t)time(NULL); |
||||
int64_t diff = now - (int64_t)ts; |
||||
if (diff < -(int64_t)cfg->time_window_sec || diff > (int64_t)cfg->time_window_sec) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: timestamp out of window ts=%u now=%lld diff=%lld", |
||||
ts, (long long)now, (long long)diff); |
||||
return REALITY_ERR_AUTH; |
||||
} |
||||
// сверка short_id
|
||||
int sid_ok = 0; |
||||
for (int i = 0; i < cfg->short_id_count; i++) { |
||||
if (memcmp(plain + 8, cfg->short_ids[i], REALITY_SHORT_ID_SIZE) == 0) { sid_ok = 1; break; } |
||||
} |
||||
if (!sid_ok) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: short_id not in list"); |
||||
return REALITY_ERR_AUTH; |
||||
} |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: auth OK short_id=%02x%02x%02x%02x%02x%02x%02x%02x ts=%u", |
||||
plain[8], plain[9], plain[10], plain[11], plain[12], plain[13], plain[14], plain[15], ts); |
||||
|
||||
// ─── собрать ServerHello ───
|
||||
const struct reality_fingerprint *fp = reality_fingerprint_get(cfg->fingerprint); |
||||
if (!fp) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: unknown fingerprint"); return REALITY_ERR_ARG; } |
||||
|
||||
uint8_t eph_priv[32], eph_pub[32], srv_random[32]; |
||||
if (reality_gen_keypair(eph_priv, eph_pub) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: keygen failed"); |
||||
return REALITY_ERR_CRYPTO; |
||||
} |
||||
(void)eph_priv; |
||||
if (random_bytes(srv_random, sizeof(srv_random)) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_server_hello_build: random_bytes failed"); |
||||
return REALITY_ERR_CRYPTO; |
||||
} |
||||
|
||||
// ServerHello body
|
||||
uint8_t sh_body[256]; |
||||
struct rbuf b = { sh_body, sizeof(sh_body), 0 }; |
||||
if (rbuf_put_u16(&b, 0x0303) < 0) return REALITY_ERR_ARG; // legacy_version
|
||||
if (rbuf_put(&b, srv_random, 32) < 0) return REALITY_ERR_ARG; // random
|
||||
if (rbuf_put_u8(&b, 32) < 0) return REALITY_ERR_ARG; // session_id_echo len
|
||||
if (rbuf_put(&b, p.session_id, 32) < 0) return REALITY_ERR_ARG; // echo SessionId клиента
|
||||
if (rbuf_put_u16(&b, fp->server_cipher) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u8(&b, 0) < 0) return REALITY_ERR_ARG; // compression = null
|
||||
|
||||
size_t ext_len_pos = b.len; |
||||
if (rbuf_put_u16(&b, 0) < 0) return REALITY_ERR_ARG; // placeholder
|
||||
// supported_versions
|
||||
if (rbuf_put_u16(&b, TLS_EXT_SUPPORTED_VERSIONS) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u16(&b, 2) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u16(&b, TLS_VERSION_1_3) < 0) return REALITY_ERR_ARG; |
||||
// key_share (server)
|
||||
if (rbuf_put_u16(&b, TLS_EXT_KEY_SHARE) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u16(&b, 36) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u16(&b, TLS_GROUP_X25519) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put_u16(&b, 32) < 0) return REALITY_ERR_ARG; |
||||
if (rbuf_put(&b, eph_pub, 32) < 0) return REALITY_ERR_ARG; |
||||
rbuf_patch_u16(&b, ext_len_pos, (uint16_t)(b.len - ext_len_pos - 2)); |
||||
size_t sh_body_len = b.len; |
||||
|
||||
// ServerHello handshake-сообщение + TLS record
|
||||
size_t sh_hs_len = 4 + sh_body_len; |
||||
out[0] = TLS_RECORD_HANDSHAKE; |
||||
out[1] = 0x03; |
||||
out[2] = 0x03; |
||||
out[3] = (uint8_t)(sh_hs_len >> 8); |
||||
out[4] = (uint8_t)(sh_hs_len); |
||||
out[5] = TLS_HANDSHAKE_SERVER_HELLO; |
||||
out[6] = (uint8_t)(sh_body_len >> 16); |
||||
out[7] = (uint8_t)(sh_body_len >> 8); |
||||
out[8] = (uint8_t)(sh_body_len); |
||||
memcpy(out + 9, sh_body, sh_body_len); |
||||
*out_len = 5 + sh_hs_len; |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "server hello built: total=%zu", *out_len); |
||||
return REALITY_OK; |
||||
} |
||||
@ -0,0 +1,116 @@
|
||||
// reality.h — REALITY-style TLS ClientHello/ServerHello camouflage для STCP
|
||||
//
|
||||
// Модуль формирует/разбирает ClientHello и ServerHello так же, как REALITY
|
||||
// (см. 3.go): аутентификация прячется в SessionId ClientHello — весь SessionId
|
||||
// (32 байта) это AES-256-GCM(ciphertext(16) + tag(16)), а открытый текст
|
||||
// (16 байт) несёт version(3) + reserved(1) + timestamp(4) + ShortId(8).
|
||||
// Ключ AuthKey = HKDF-SHA256(X25519(eph, static), Random[0:20], "REALITY").
|
||||
//
|
||||
// Область применения — «hello-only»: обмениваемся только заголовками, реальную
|
||||
// взаимную аутентификацию делает штатный STCP-хендшейк поверх.
|
||||
#ifndef REALITY_H |
||||
#define REALITY_H |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
|
||||
// Размеры
|
||||
#define REALITY_SHORT_ID_SIZE 8 |
||||
#define REALITY_VERSION_SIZE 3 |
||||
#define REALITY_AUTH_KEY_SIZE 32 |
||||
#define REALITY_RANDOM_SIZE 32 |
||||
#define REALITY_SESSION_ID_SIZE 32 |
||||
#define REALITY_TAG_SIZE 16 |
||||
#define REALITY_MAX_CH_SIZE 2048 // максимальный размер ClientHello (TLS record + handshake)
|
||||
#define REALITY_MAX_SH_SIZE 512 // максимальный размер ServerHello
|
||||
#define REALITY_MAX_SHORT_IDS 64 |
||||
#define REALITY_SERVER_NAME_MAX 256 |
||||
|
||||
// Коды возврата
|
||||
#define REALITY_OK 0 |
||||
#define REALITY_ERR_ARG -1 |
||||
#define REALITY_ERR_CRYPTO -2 |
||||
#define REALITY_ERR_AUTH -3 |
||||
#define REALITY_ERR_FORMAT -4 |
||||
|
||||
// Отпечатки (reality_fingerprint.c)
|
||||
#define REALITY_FP_CHROME 0 |
||||
#define REALITY_FP_COUNT 1 |
||||
|
||||
// Конфигурация клиента
|
||||
struct reality_client_config { |
||||
uint8_t server_static_pubkey[REALITY_AUTH_KEY_SIZE]; // X25519 static pubkey сервера
|
||||
uint8_t short_id[REALITY_SHORT_ID_SIZE]; |
||||
uint8_t version[REALITY_VERSION_SIZE]; // версия протокола utun
|
||||
char server_name[REALITY_SERVER_NAME_MAX]; // SNI-таргет (напр. "www.microsoft.com")
|
||||
int fingerprint; // REALITY_FP_*
|
||||
}; |
||||
|
||||
// Конфигурация сервера
|
||||
struct reality_server_config { |
||||
uint8_t static_privkey[REALITY_AUTH_KEY_SIZE]; // X25519 static privkey сервера
|
||||
uint8_t short_ids[REALITY_MAX_SHORT_IDS][REALITY_SHORT_ID_SIZE]; |
||||
int short_id_count; |
||||
uint8_t version[REALITY_VERSION_SIZE]; // принимаемая версия
|
||||
int64_t time_window_sec; // допуск timestamp (антиреплей)
|
||||
int fingerprint; |
||||
}; |
||||
|
||||
// Общая конфигурация reality (секция [reality] в конфиге utun)
|
||||
#define REALITY_DEST_MAX 256 |
||||
struct reality_config { |
||||
int enabled; // 1 = камуфляж включён
|
||||
char server_name[REALITY_SERVER_NAME_MAX]; // SNI-таргет
|
||||
char dest[REALITY_DEST_MAX]; // "host:port" для релея (fallback)
|
||||
uint8_t short_id[REALITY_SHORT_ID_SIZE]; // клиентский short_id
|
||||
uint8_t short_ids[REALITY_MAX_SHORT_IDS][REALITY_SHORT_ID_SIZE]; // сервер: список
|
||||
int short_id_count; |
||||
uint8_t public_key[REALITY_AUTH_KEY_SIZE]; // клиент: static pubkey сервера
|
||||
uint8_t private_key[REALITY_AUTH_KEY_SIZE]; // сервер: static privkey
|
||||
uint8_t has_public_key; |
||||
uint8_t has_private_key; |
||||
uint8_t version[REALITY_VERSION_SIZE]; // версия протокола
|
||||
int time_window_sec; |
||||
int fingerprint; |
||||
}; |
||||
|
||||
// Заполняет cfg->version/fingerprint/time_window значениями по умолчанию
|
||||
// (version=1.0.0, fingerprint=chrome, time_window=30). Вызывается при инициализации.
|
||||
void reality_config_set_defaults(struct reality_config *cfg); |
||||
|
||||
// Клиент: собрать ClientHello (TLS record + handshake) с REALITY-авторизацией.
|
||||
// Генерирует эфемерный X25519 и Random внутри. out_cap >= REALITY_MAX_CH_SIZE.
|
||||
// Возвращает REALITY_OK / REALITY_ERR_*, в *out_len — итоговый размер.
|
||||
int reality_client_hello_build(const struct reality_client_config *cfg, |
||||
uint8_t *out, size_t out_cap, size_t *out_len); |
||||
|
||||
// То же, но с явным timestamp (unix-секунды) — для тестов антиреплей-проверки.
|
||||
int reality_client_hello_build_at(const struct reality_client_config *cfg, uint32_t ts, |
||||
uint8_t *out, size_t out_cap, size_t *out_len); |
||||
|
||||
// Сервер: распарсить ClientHello, проверить авторизацию, собрать ServerHello.
|
||||
// ch/ch_len — принятые байты (TLS record + handshake, допускается одна запись).
|
||||
// REALITY_OK — авторизация валидна, out/out_len = ServerHello.
|
||||
// REALITY_ERR_AUTH — авторизация не прошла (вызывающий код запускает релей).
|
||||
// REALITY_ERR_* — формат/аргумент/крипто-ошибка.
|
||||
int reality_server_hello_build(const struct reality_server_config *cfg, |
||||
const uint8_t *ch, size_t ch_len, |
||||
uint8_t *out, size_t out_cap, size_t *out_len); |
||||
|
||||
// Сгенерировать X25519 пару ключей (для конфигурации: сервер хранит privkey,
|
||||
// клиент — pubkey сервера). Возвращает REALITY_OK / REALITY_ERR_CRYPTO.
|
||||
int reality_generate_keypair(uint8_t priv[REALITY_AUTH_KEY_SIZE], |
||||
uint8_t pub[REALITY_AUTH_KEY_SIZE]); |
||||
|
||||
// Вычислить X25519 pubkey из privkey. Возвращает REALITY_OK / REALITY_ERR_CRYPTO.
|
||||
int reality_pubkey_from_priv(const uint8_t priv[REALITY_AUTH_KEY_SIZE], |
||||
uint8_t pub[REALITY_AUTH_KEY_SIZE]); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif // REALITY_H
|
||||
@ -0,0 +1,47 @@
|
||||
// reality_fingerprint.c — статические отпечатки для REALITY-камуфляжа
|
||||
#include "reality_fingerprint.h" |
||||
#include "reality.h" |
||||
#include <stddef.h> |
||||
|
||||
// Chrome-like TLS 1.3 отпечаток (упрощённый, но валидный набор).
|
||||
// Cipher suites: 3 × TLS 1.3 + несколько legacy ECDHE-суитов для правдоподобия.
|
||||
static const uint16_t fp_chrome_cipher_suites[] = { |
||||
0x1301, 0x1302, 0x1303, // TLS_AES_128/256_GCM, CHACHA20
|
||||
0xc02c, 0xc02b, 0xc030, 0xc02f, // ECDHE-ECDSA/RSA AES-256/128-GCM
|
||||
0xcca9, 0xcca8, // ECDHE-ECDSA/RSA CHACHA20-POLY1305
|
||||
0x0000 |
||||
}; |
||||
|
||||
static const uint16_t fp_chrome_supported_groups[] = { |
||||
0x001d, // X25519
|
||||
0x0017, 0x0018, 0x0019, // secp256r1/384r1/521r1
|
||||
0x0000 |
||||
}; |
||||
|
||||
static const uint16_t fp_chrome_sig_algs[] = { |
||||
0x0804, 0x0805, 0x0806, // rsa_pss_rsae_sha256/384/512
|
||||
0x0401, 0x0501, 0x0601, // rsa_pkcs1_sha256/384/512
|
||||
0x0403, 0x0503, 0x0603, // ecdsa_secp256r1/384r1/521r1
|
||||
0x0807, 0x0808, // ed25519, ed448
|
||||
0x0000 |
||||
}; |
||||
|
||||
static const char *const fp_chrome_alpn[] = { "h2", "http/1.1", NULL }; |
||||
|
||||
static const struct reality_fingerprint g_fingerprints[REALITY_FP_COUNT] = { |
||||
{ |
||||
.id = REALITY_FP_CHROME, |
||||
.name = "chrome", |
||||
.cipher_suites = fp_chrome_cipher_suites, |
||||
.supported_groups = fp_chrome_supported_groups, |
||||
.sig_algs = fp_chrome_sig_algs, |
||||
.alpn = fp_chrome_alpn, |
||||
.server_cipher = 0x1301, // TLS_AES_128_GCM_SHA256
|
||||
}, |
||||
}; |
||||
|
||||
const struct reality_fingerprint *reality_fingerprint_get(int id) { |
||||
for (int i = 0; i < REALITY_FP_COUNT; i++) |
||||
if (g_fingerprints[i].id == id) return &g_fingerprints[i]; |
||||
return NULL; |
||||
} |
||||
@ -0,0 +1,32 @@
|
||||
// reality_fingerprint.h — статические отпечатки (fingerprints) для REALITY-камуфляжа
|
||||
//
|
||||
// Отпечаток описывает статический скелет ClientHello/ServerHello: список cipher
|
||||
// suites, supported groups, signature algorithms, ALPN и выбранный cipher suite
|
||||
// для ServerHello. Билдер (reality.c) подставляет динамические поля (Random,
|
||||
// SessionId, key_share, SNI) в этот скелет.
|
||||
#ifndef REALITY_FINGERPRINT_H |
||||
#define REALITY_FINGERPRINT_H |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
#include <stdint.h> |
||||
|
||||
struct reality_fingerprint { |
||||
int id; |
||||
const char *name; |
||||
const uint16_t *cipher_suites; // список, терминирован 0x0000
|
||||
const uint16_t *supported_groups; // список, терминирован 0x0000
|
||||
const uint16_t *sig_algs; // список, терминирован 0x0000
|
||||
const char *const *alpn; // NULL-терминированный список
|
||||
uint16_t server_cipher; // выбранный cipher suite для ServerHello
|
||||
}; |
||||
|
||||
// Возвращает отпечаток по id (REALITY_FP_*) или NULL, если не найден.
|
||||
const struct reality_fingerprint *reality_fingerprint_get(int id); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif // REALITY_FINGERPRINT_H
|
||||
@ -0,0 +1,263 @@
|
||||
// reality_relay.c — живой релей неавторизованных клиентов на реальный HTTPS-сайт
|
||||
//
|
||||
// Двухсторонний TCP-пайп: клиент (приславший невалидный ClientHello) ↔ реальный
|
||||
// сайт (dest). initial_data (уже прочитанный ClientHello) пересылается на dest
|
||||
// первым. Поддержан half-close: FIN с одной стороны закрывает только свою
|
||||
// сторону записи на другой, чтобы ответ сайта дошёл до клиента полностью.
|
||||
#include "reality_relay.h" |
||||
#include "../lib/u_async.h" |
||||
#include "../lib/mem.h" |
||||
#include "../lib/debug_config.h" |
||||
#include "../lib/platform_compat.h" |
||||
#include <string.h> |
||||
#include <stdlib.h> |
||||
#include <errno.h> |
||||
#ifndef _WIN32 |
||||
#include <netinet/tcp.h> |
||||
#endif |
||||
|
||||
#define RELAY_IO_BUF 65536 |
||||
|
||||
#ifdef _WIN32 |
||||
#define RELAY_SHUT_WR SD_SEND |
||||
#else |
||||
#define RELAY_SHUT_WR SHUT_WR |
||||
#endif |
||||
|
||||
struct reality_relay { |
||||
struct UASYNC *ua; |
||||
socket_t client_sock; |
||||
void *client_sid; |
||||
socket_t dest_sock; |
||||
void *dest_sid; |
||||
int connecting; // 1 = ждём завершения connect() к dest
|
||||
|
||||
uint8_t *c2d; size_t c2d_len, c2d_off; // pending client→dest
|
||||
uint8_t *d2c; size_t d2c_len, d2c_off; // pending dest→client
|
||||
|
||||
int client_read_eof; // клиент закрыл свою сторону записи
|
||||
int dest_read_eof; // dest закрыл свою сторону записи
|
||||
int closed; |
||||
|
||||
size_t bytes_c2d, bytes_d2c; |
||||
}; |
||||
|
||||
static void relay_free(struct reality_relay *r); |
||||
static void relay_flush_c2d(struct reality_relay *r); |
||||
static void relay_flush_d2c(struct reality_relay *r); |
||||
static void relay_client_read_cb(socket_t sock, void *arg); |
||||
static void relay_dest_read_cb(socket_t sock, void *arg); |
||||
static void relay_client_write_cb(socket_t sock, void *arg); |
||||
static void relay_dest_write_cb(socket_t sock, void *arg); |
||||
|
||||
static void relay_free_cb(void *arg) { |
||||
u_free(arg); |
||||
} |
||||
|
||||
static void relay_free(struct reality_relay *r) { |
||||
if (!r || r->closed) return; |
||||
r->closed = 1; |
||||
if (r->client_sid) { uasync_remove_socket_t(r->ua, r->client_sock); r->client_sid = NULL; } |
||||
if (r->dest_sid) { uasync_remove_socket_t(r->ua, r->dest_sock); r->dest_sid = NULL; } |
||||
if (r->client_sock != SOCKET_INVALID) { socket_close_wrapper(r->client_sock); r->client_sock = SOCKET_INVALID; } |
||||
if (r->dest_sock != SOCKET_INVALID) { socket_close_wrapper(r->dest_sock); r->dest_sock = SOCKET_INVALID; } |
||||
if (r->c2d) { u_free(r->c2d); r->c2d = NULL; } |
||||
if (r->d2c) { u_free(r->d2c); r->d2c = NULL; } |
||||
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "reality_relay closed: c2d=%zu d2c=%zu", r->bytes_c2d, r->bytes_d2c); |
||||
uasync_call_soon(r->ua, r, relay_free_cb); |
||||
} |
||||
|
||||
static void relay_flush_c2d(struct reality_relay *r) { |
||||
if (!r->c2d) return; |
||||
while (r->c2d_off < r->c2d_len) { |
||||
ssize_t sent = send(r->dest_sock, r->c2d + r->c2d_off, r->c2d_len - r->c2d_off, 0); |
||||
if (sent < 0) { |
||||
int err = socket_get_error(); |
||||
if (err == ERR_AGAIN || err == ERR_WOULDBLOCK) { uasync_set_socket_write(r->ua, r->dest_sid, 1); return; } |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_relay send to dest failed err=%d", err); |
||||
relay_free(r); return; |
||||
} |
||||
if (sent == 0) { relay_free(r); return; } |
||||
r->c2d_off += (size_t)sent; |
||||
} |
||||
r->bytes_c2d += r->c2d_len; |
||||
u_free(r->c2d); r->c2d = NULL; |
||||
uasync_set_socket_write(r->ua, r->dest_sid, 0); |
||||
if (!r->client_read_eof) uasync_set_socket_read(r->ua, r->client_sid, 1); |
||||
} |
||||
|
||||
static void relay_flush_d2c(struct reality_relay *r) { |
||||
if (!r->d2c) return; |
||||
while (r->d2c_off < r->d2c_len) { |
||||
ssize_t sent = send(r->client_sock, r->d2c + r->d2c_off, r->d2c_len - r->d2c_off, 0); |
||||
if (sent < 0) { |
||||
int err = socket_get_error(); |
||||
if (err == ERR_AGAIN || err == ERR_WOULDBLOCK) { uasync_set_socket_write(r->ua, r->client_sid, 1); return; } |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_relay send to client failed err=%d", err); |
||||
relay_free(r); return; |
||||
} |
||||
if (sent == 0) { relay_free(r); return; } |
||||
r->d2c_off += (size_t)sent; |
||||
} |
||||
r->bytes_d2c += r->d2c_len; |
||||
u_free(r->d2c); r->d2c = NULL; |
||||
uasync_set_socket_write(r->ua, r->client_sid, 0); |
||||
if (!r->dest_read_eof) uasync_set_socket_read(r->ua, r->dest_sid, 1); |
||||
} |
||||
|
||||
static void relay_client_read_cb(socket_t sock, void *arg) { |
||||
struct reality_relay *r = (struct reality_relay *)arg; |
||||
(void)sock; |
||||
if (r->c2d) return; |
||||
uint8_t *buf = u_malloc(RELAY_IO_BUF); |
||||
if (!buf) { relay_free(r); return; } |
||||
ssize_t n = recv(r->client_sock, buf, RELAY_IO_BUF, 0); |
||||
if (n < 0) { |
||||
int err = socket_get_error(); |
||||
if (err == ERR_AGAIN || err == ERR_WOULDBLOCK) { u_free(buf); return; } |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_relay recv from client failed err=%d", err); |
||||
u_free(buf); relay_free(r); return; |
||||
} |
||||
if (n == 0) { |
||||
u_free(buf); |
||||
r->client_read_eof = 1; |
||||
uasync_set_socket_read(r->ua, r->client_sid, 0); |
||||
shutdown(r->dest_sock, RELAY_SHUT_WR); |
||||
if (r->dest_read_eof) relay_free(r); |
||||
return; |
||||
} |
||||
r->c2d = buf; r->c2d_len = (size_t)n; r->c2d_off = 0; |
||||
uasync_set_socket_read(r->ua, r->client_sid, 0); // backpressure
|
||||
relay_flush_c2d(r); |
||||
} |
||||
|
||||
static void relay_dest_read_cb(socket_t sock, void *arg) { |
||||
struct reality_relay *r = (struct reality_relay *)arg; |
||||
(void)sock; |
||||
if (r->d2c) return; |
||||
uint8_t *buf = u_malloc(RELAY_IO_BUF); |
||||
if (!buf) { relay_free(r); return; } |
||||
ssize_t n = recv(r->dest_sock, buf, RELAY_IO_BUF, 0); |
||||
if (n < 0) { |
||||
int err = socket_get_error(); |
||||
if (err == ERR_AGAIN || err == ERR_WOULDBLOCK) { u_free(buf); return; } |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_relay recv from dest failed err=%d", err); |
||||
u_free(buf); relay_free(r); return; |
||||
} |
||||
if (n == 0) { |
||||
u_free(buf); |
||||
r->dest_read_eof = 1; |
||||
uasync_set_socket_read(r->ua, r->dest_sid, 0); |
||||
shutdown(r->client_sock, RELAY_SHUT_WR); |
||||
if (r->client_read_eof) relay_free(r); |
||||
return; |
||||
} |
||||
r->d2c = buf; r->d2c_len = (size_t)n; r->d2c_off = 0; |
||||
uasync_set_socket_read(r->ua, r->dest_sid, 0); // backpressure
|
||||
relay_flush_d2c(r); |
||||
} |
||||
|
||||
static void relay_client_write_cb(socket_t sock, void *arg) { |
||||
struct reality_relay *r = (struct reality_relay *)arg; |
||||
(void)sock; |
||||
if (!r->d2c) { uasync_set_socket_write(r->ua, r->client_sid, 0); return; } |
||||
relay_flush_d2c(r); |
||||
} |
||||
|
||||
static void relay_dest_write_cb(socket_t sock, void *arg) { |
||||
struct reality_relay *r = (struct reality_relay *)arg; |
||||
if (r->connecting) { |
||||
int err = 0; socklen_t elen = sizeof(err); |
||||
if (getsockopt(sock, SOL_SOCKET, SO_ERROR, (char *)&err, &elen) < 0 || err != 0) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_relay connect to dest failed err=%d", err); |
||||
relay_free(r); return; |
||||
} |
||||
r->connecting = 0; |
||||
int opt = 1; setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&opt, sizeof(opt)); |
||||
if (r->c2d) relay_flush_c2d(r); |
||||
else { uasync_set_socket_write(r->ua, r->dest_sid, 0); uasync_set_socket_read(r->ua, r->client_sid, 1); } |
||||
return; |
||||
} |
||||
if (!r->c2d) { uasync_set_socket_write(r->ua, r->dest_sid, 0); return; } |
||||
relay_flush_c2d(r); |
||||
} |
||||
|
||||
int reality_relay_start(struct UASYNC *ua, socket_t client_sock, |
||||
const char *dest, |
||||
const uint8_t *initial_data, size_t initial_len) { |
||||
if (!ua || client_sock == SOCKET_INVALID || !dest || !dest[0]) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_relay_start: invalid args"); |
||||
return -1; |
||||
} |
||||
|
||||
// парсим "host:port"
|
||||
char host[256]; |
||||
uint16_t port = 443; |
||||
{ |
||||
size_t dl = strlen(dest); |
||||
if (dl >= sizeof(host)) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_relay_start: dest too long"); goto fail; } |
||||
memcpy(host, dest, dl + 1); |
||||
char *colon = strrchr(host, ':'); |
||||
if (colon) { *colon = '\0'; port = (uint16_t)atoi(colon + 1); } |
||||
if (!host[0] || port == 0) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_relay_start: bad dest '%s'", dest); goto fail; } |
||||
} |
||||
|
||||
struct addrinfo hints = {0}; |
||||
hints.ai_family = AF_UNSPEC; |
||||
hints.ai_socktype = SOCK_STREAM; |
||||
char port_str[16]; |
||||
snprintf(port_str, sizeof(port_str), "%u", port); |
||||
struct addrinfo *res; |
||||
if (getaddrinfo(host, port_str, &hints, &res) != 0) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_relay: getaddrinfo %s failed", host); |
||||
goto fail; |
||||
} |
||||
|
||||
socket_t dest_sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol); |
||||
if (dest_sock == SOCKET_INVALID) { freeaddrinfo(res); DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_relay: socket failed"); goto fail; } |
||||
socket_set_nonblocking(dest_sock); |
||||
|
||||
struct reality_relay *r = u_calloc(1, sizeof(struct reality_relay)); |
||||
if (!r) { socket_close_wrapper(dest_sock); freeaddrinfo(res); DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "reality_relay: calloc failed"); goto fail; } |
||||
r->ua = ua; |
||||
r->client_sock = client_sock; |
||||
r->dest_sock = dest_sock; |
||||
r->connecting = 1; |
||||
|
||||
// берём владение client_sock: снимаем прежнюю регистрацию и ставим свою
|
||||
uasync_remove_socket_t(ua, client_sock); |
||||
r->client_sid = uasync_add_socket_t(ua, client_sock, relay_client_read_cb, relay_client_write_cb, NULL, r); |
||||
if (!r->client_sid) { relay_free(r); freeaddrinfo(res); return -1; } |
||||
|
||||
if (initial_len && initial_data) { |
||||
r->c2d = u_malloc(initial_len); |
||||
if (!r->c2d) { relay_free(r); freeaddrinfo(res); return -1; } |
||||
memcpy(r->c2d, initial_data, initial_len); |
||||
r->c2d_len = initial_len; |
||||
r->c2d_off = 0; |
||||
} |
||||
|
||||
int cr = connect(dest_sock, res->ai_addr, res->ai_addrlen); |
||||
freeaddrinfo(res); |
||||
if (cr < 0) { |
||||
int err = socket_get_error(); |
||||
if (err != EINPROGRESS && err != ERR_WOULDBLOCK) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "reality_relay connect %s:%u failed err=%d", host, port, err); |
||||
relay_free(r); return -1; |
||||
} |
||||
} else { |
||||
r->connecting = 0; |
||||
int opt = 1; setsockopt(dest_sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&opt, sizeof(opt)); |
||||
} |
||||
r->dest_sid = uasync_add_socket_t(ua, dest_sock, relay_dest_read_cb, relay_dest_write_cb, NULL, r); |
||||
if (!r->dest_sid) { relay_free(r); return -1; } |
||||
|
||||
if (!r->connecting && r->c2d) relay_flush_c2d(r); |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "reality_relay started: client→%s:%u initial=%zu", host, port, initial_len); |
||||
return 0; |
||||
|
||||
fail: |
||||
socket_close_wrapper(client_sock); |
||||
return -1; |
||||
} |
||||
@ -0,0 +1,32 @@
|
||||
// reality_relay.h — живой релей неавторизованных клиентов на реальный HTTPS-сайт
|
||||
//
|
||||
// Используется сервером STCP при включённом reality: если авторизация ClientHello
|
||||
// не прошла (не utun-клиент — DPI-проба, случайный посетитель), соединение
|
||||
// прозрачно проксируется на реальный сайт (dest), чтобы проба видела настоящий
|
||||
// HTTPS-сеанс, а не ошибку/разрыв.
|
||||
#ifndef REALITY_RELAY_H |
||||
#define REALITY_RELAY_H |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
#include "../lib/socket_compat.h" |
||||
|
||||
struct UASYNC; |
||||
|
||||
// Начать релей. Забирает владение client_sock (non-blocking, зарегистрирован
|
||||
// в uasync) и проксирует его на dest ("host:port").
|
||||
// initial_data/initial_len — уже прочитанные байты от клиента (ClientHello),
|
||||
// отправляются на dest первыми (буфер переходит во владение модуля).
|
||||
// Возвращает 0 = ok, -1 = ошибка (client_sock уже закрыт внутри).
|
||||
int reality_relay_start(struct UASYNC *ua, socket_t client_sock, |
||||
const char *dest, |
||||
const uint8_t *initial_data, size_t initial_len); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif // REALITY_RELAY_H
|
||||
@ -0,0 +1,214 @@
|
||||
// test_reality_hello.c — тесты модуля reality (REALITY-style ClientHello/ServerHello)
|
||||
//
|
||||
// Сценарии:
|
||||
// 1. Round-trip: клиент собирает ClientHello → сервер проверяет авторизацию и
|
||||
// собирает ServerHello; проверка структуры обоих (TLS record + handshake,
|
||||
// echo SessionId).
|
||||
// 2. Неверный short_id → REALITY_ERR_AUTH.
|
||||
// 3. Неверный static privkey сервера → REALITY_ERR_AUTH (AES-GCM не сходится).
|
||||
// 4. Порча байта ClientHello → REALITY_ERR_AUTH (AAD не совпадает).
|
||||
// 5. Несовпадение версии протокола → REALITY_ERR_AUTH.
|
||||
// 6. Timestamp вне окна (антиреплей) → REALITY_ERR_AUTH; в окне → OK.
|
||||
// 7. Ошибки формата (мусор/пусто/неверный тип) → REALITY_ERR_FORMAT.
|
||||
// 8. Fingerprint-геттер и keygen/pubkey_from_priv.
|
||||
#include "reality.h" |
||||
#include "reality_fingerprint.h" |
||||
#include "config_parser.h" |
||||
#include "../lib/debug_config.h" |
||||
#include <stdio.h> |
||||
#include <string.h> |
||||
|
||||
static int test_failed = 0; |
||||
|
||||
#define CHECK(expr, msg) do { \ |
||||
if (!(expr)) { \
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "FAIL: %s", msg); \
|
||||
test_failed = 1; \
|
||||
} else { \
|
||||
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "PASS: %s", msg); \
|
||||
} \
|
||||
} while (0) |
||||
|
||||
static uint8_t g_short_id[REALITY_SHORT_ID_SIZE] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; |
||||
static uint8_t g_short_id_wrong[REALITY_SHORT_ID_SIZE] = { 0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8 }; |
||||
static uint8_t g_version[REALITY_VERSION_SIZE] = { 1, 2, 3 }; |
||||
static uint8_t g_version_wrong[REALITY_VERSION_SIZE] = { 9, 9, 9 }; |
||||
|
||||
static void init_client_cfg(struct reality_client_config *cc, |
||||
const uint8_t *server_pub, |
||||
const uint8_t *short_id, |
||||
const uint8_t *version) { |
||||
memset(cc, 0, sizeof(*cc)); |
||||
memcpy(cc->server_static_pubkey, server_pub, REALITY_AUTH_KEY_SIZE); |
||||
memcpy(cc->short_id, short_id, REALITY_SHORT_ID_SIZE); |
||||
memcpy(cc->version, version, REALITY_VERSION_SIZE); |
||||
snprintf(cc->server_name, sizeof(cc->server_name), "www.microsoft.com"); |
||||
cc->fingerprint = REALITY_FP_CHROME; |
||||
} |
||||
|
||||
static void init_server_cfg(struct reality_server_config *sc, |
||||
const uint8_t *server_priv, |
||||
const uint8_t *short_id, |
||||
const uint8_t *version, |
||||
int64_t window) { |
||||
memset(sc, 0, sizeof(*sc)); |
||||
memcpy(sc->static_privkey, server_priv, REALITY_AUTH_KEY_SIZE); |
||||
memcpy(sc->short_ids[0], short_id, REALITY_SHORT_ID_SIZE); |
||||
sc->short_id_count = 1; |
||||
memcpy(sc->version, version, REALITY_VERSION_SIZE); |
||||
sc->time_window_sec = window; |
||||
sc->fingerprint = REALITY_FP_CHROME; |
||||
} |
||||
|
||||
int main(void) { |
||||
debug_config_init(); |
||||
debug_set_level(DEBUG_LEVEL_INFO); |
||||
debug_set_categories(DEBUG_CATEGORY_REALITY); |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "=== Reality Hello Test ==="); |
||||
|
||||
uint8_t srv_priv[REALITY_AUTH_KEY_SIZE], srv_pub[REALITY_AUTH_KEY_SIZE]; |
||||
CHECK(reality_generate_keypair(srv_priv, srv_pub) == REALITY_OK, "generate server keypair"); |
||||
{ |
||||
uint8_t pub2[REALITY_AUTH_KEY_SIZE]; |
||||
CHECK(reality_pubkey_from_priv(srv_priv, pub2) == REALITY_OK, "pubkey_from_priv"); |
||||
CHECK(memcmp(srv_pub, pub2, REALITY_AUTH_KEY_SIZE) == 0, "pubkey_from_priv matches"); |
||||
} |
||||
|
||||
uint8_t other_priv[REALITY_AUTH_KEY_SIZE], other_pub[REALITY_AUTH_KEY_SIZE]; |
||||
CHECK(reality_generate_keypair(other_priv, other_pub) == REALITY_OK, "generate wrong keypair"); |
||||
|
||||
struct reality_client_config cc; |
||||
struct reality_server_config sc; |
||||
uint8_t ch[REALITY_MAX_CH_SIZE], sh[REALITY_MAX_SH_SIZE]; |
||||
size_t ch_len = 0, sh_len = 0; |
||||
|
||||
// ── Сценарий 1: round-trip ──
|
||||
init_client_cfg(&cc, srv_pub, g_short_id, g_version); |
||||
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60); |
||||
|
||||
CHECK(reality_client_hello_build(&cc, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build"); |
||||
CHECK(ch_len > 5, "client hello non-trivial size"); |
||||
CHECK(ch[0] == 0x16 && ch[1] == 0x03, "client hello TLS record header"); |
||||
CHECK(ch[5] == 0x01, "client hello handshake type"); |
||||
{ |
||||
// проверка согласованности длины: record length == handshake length + 4
|
||||
size_t rec_len = ((size_t)ch[3] << 8) | ch[4]; |
||||
CHECK(rec_len + 5 == ch_len, "client hello record length matches"); |
||||
} |
||||
|
||||
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_OK, "server hello build (auth OK)"); |
||||
CHECK(sh[0] == 0x16 && sh[5] == 0x02, "server hello record + handshake type"); |
||||
{ |
||||
// echo SessionId: client SessionId в ch[44..76], server echo в sh[44..76]
|
||||
CHECK(memcmp(ch + 44, sh + 44, REALITY_SESSION_ID_SIZE) == 0, "server echoes client SessionId"); |
||||
} |
||||
|
||||
// ── Сценарий 2: неверный short_id ──
|
||||
init_client_cfg(&cc, srv_pub, g_short_id_wrong, g_version); |
||||
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60); |
||||
CHECK(reality_client_hello_build(&cc, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (wrong sid)"); |
||||
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "wrong short_id rejected"); |
||||
|
||||
// ── Сценарий 3: неверный static privkey сервера ──
|
||||
init_client_cfg(&cc, srv_pub, g_short_id, g_version); |
||||
init_server_cfg(&sc, other_priv, g_short_id, g_version, 60); |
||||
CHECK(reality_client_hello_build(&cc, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (wrong key)"); |
||||
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "wrong server key rejected"); |
||||
|
||||
// ── Сценарий 4: порча байта ClientHello ──
|
||||
init_client_cfg(&cc, srv_pub, g_short_id, g_version); |
||||
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60); |
||||
CHECK(reality_client_hello_build(&cc, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (tamper)"); |
||||
ch[50] ^= 0x01; // портим байт внутри handshake-сообщения
|
||||
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "tampered hello rejected"); |
||||
|
||||
// ── Сценарий 5: несовпадение версии ──
|
||||
init_client_cfg(&cc, srv_pub, g_short_id, g_version_wrong); |
||||
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60); |
||||
CHECK(reality_client_hello_build(&cc, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (ver mismatch)"); |
||||
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "version mismatch rejected"); |
||||
|
||||
// ── Сценарий 6: timestamp вне окна ──
|
||||
init_client_cfg(&cc, srv_pub, g_short_id, g_version); |
||||
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60); |
||||
{ |
||||
uint32_t now = (uint32_t)time(NULL); |
||||
CHECK(reality_client_hello_build_at(&cc, now, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (ts=now)"); |
||||
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_OK, "ts=now accepted"); |
||||
|
||||
CHECK(reality_client_hello_build_at(&cc, now - 3600, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (ts=past)"); |
||||
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "ts=past rejected (anti-replay)"); |
||||
|
||||
CHECK(reality_client_hello_build_at(&cc, now + 3600, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (ts=future)"); |
||||
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "ts=future rejected"); |
||||
} |
||||
|
||||
// ── Сценарий 7: ошибки формата ──
|
||||
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60); |
||||
{ |
||||
uint8_t garbage[REALITY_MAX_CH_SIZE]; |
||||
memset(garbage, 0, sizeof(garbage)); |
||||
CHECK(reality_server_hello_build(&sc, garbage, 0, sh, sizeof(sh), &sh_len) == REALITY_ERR_FORMAT, "empty input rejected"); |
||||
CHECK(reality_server_hello_build(&sc, garbage, 100, sh, sizeof(sh), &sh_len) == REALITY_ERR_FORMAT, "all-zero input rejected"); |
||||
|
||||
memset(garbage, 0, sizeof(garbage)); |
||||
garbage[0] = 0x17; // не handshake-запись
|
||||
CHECK(reality_server_hello_build(&sc, garbage, 100, sh, sizeof(sh), &sh_len) == REALITY_ERR_FORMAT, "wrong record type rejected"); |
||||
|
||||
// валидная запись, но не ClientHello
|
||||
memset(garbage, 0, sizeof(garbage)); |
||||
garbage[0] = 0x16; garbage[1] = 0x03; garbage[2] = 0x01; |
||||
garbage[3] = 0x00; garbage[4] = 0x04; // record length 4
|
||||
garbage[5] = 0x02; // handshake type = server_hello
|
||||
CHECK(reality_server_hello_build(&sc, garbage, 9, sh, sizeof(sh), &sh_len) == REALITY_ERR_FORMAT, "non-client-hello rejected"); |
||||
} |
||||
|
||||
// ── Сценарий 8: fingerprint-геттер ──
|
||||
CHECK(reality_fingerprint_get(REALITY_FP_CHROME) != NULL, "fingerprint chrome found"); |
||||
CHECK(reality_fingerprint_get(999) == NULL, "unknown fingerprint = NULL"); |
||||
|
||||
// ── Сценарий 9: невалидные аргументы ──
|
||||
init_client_cfg(&cc, srv_pub, g_short_id, g_version); |
||||
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60); |
||||
CHECK(reality_client_hello_build(NULL, ch, sizeof(ch), &ch_len) == REALITY_ERR_ARG, "client build NULL cfg rejected"); |
||||
CHECK(reality_client_hello_build(&cc, ch, 16, &ch_len) == REALITY_ERR_ARG, "client build small buffer rejected"); |
||||
CHECK(reality_server_hello_build(&sc, NULL, 0, sh, sizeof(sh), &sh_len) == REALITY_ERR_ARG, "server build NULL input rejected"); |
||||
|
||||
// ── Сценарий 10: парсинг секции [reality] из конфига ──
|
||||
{ |
||||
const char *cfg_text = |
||||
"[global]\nname=test\n" |
||||
"[reality]\n" |
||||
"enabled=1\n" |
||||
"server_name=www.microsoft.com\n" |
||||
"dest=www.microsoft.com:443\n" |
||||
"short_id=0102030405060708\n" |
||||
"short_ids=0102030405060708,aabbccddeeff0011\n" |
||||
"public_key=1111111111111111111111111111111111111111111111111111111111111111\n" |
||||
"private_key=2222222222222222222222222222222222222222222222222222222222222222\n" |
||||
"version=2.3.4\n" |
||||
"time_window=45\n" |
||||
"fingerprint=chrome\n"; |
||||
struct utun_config *uc = parse_config_from_buf(cfg_text, strlen(cfg_text), "mem"); |
||||
CHECK(uc != NULL, "parse config with [reality]"); |
||||
if (uc) { |
||||
CHECK(uc->global.reality.enabled == 1, "reality enabled parsed"); |
||||
CHECK(strcmp(uc->global.reality.server_name, "www.microsoft.com") == 0, "server_name parsed"); |
||||
CHECK(strcmp(uc->global.reality.dest, "www.microsoft.com:443") == 0, "dest parsed"); |
||||
CHECK(uc->global.reality.has_public_key == 1 && uc->global.reality.has_private_key == 1, "pub/priv keys parsed"); |
||||
CHECK(uc->global.reality.short_id_count == 2, "short_ids parsed"); |
||||
CHECK(uc->global.reality.version[0] == 2 && uc->global.reality.version[1] == 3 && uc->global.reality.version[2] == 4, "version parsed"); |
||||
CHECK(uc->global.reality.time_window_sec == 45, "time_window parsed"); |
||||
CHECK(uc->global.reality.fingerprint == REALITY_FP_CHROME, "fingerprint parsed"); |
||||
free_config(uc); |
||||
} |
||||
} |
||||
|
||||
if (test_failed) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "=== Reality Hello Test: FAILED ==="); |
||||
return 1; |
||||
} |
||||
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "=== Reality Hello Test: PASSED ==="); |
||||
return 0; |
||||
} |
||||
Loading…
Reference in new issue