33 changed files with 1786 additions and 421 deletions
@ -1,47 +1,16 @@
|
||||
// reality_fingerprint.c — статические отпечатки для REALITY-камуфляжа
|
||||
/* Профиль OpenSSL 3.5.5: TLS 1.3, X25519, AES-128-GCM, ALPN http/1.1.
|
||||
* Эталон и команда воспроизведения: tests/fixtures/reality_openssl_hello.hex. */ |
||||
#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 suites[] = {0x1301, 0}; |
||||
static const uint16_t groups[] = {0x001d, 0}; |
||||
static const uint16_t signatures[] = { |
||||
0x0905, 0x0906, 0x0904, 0x0403, 0x0503, 0x0603, 0x0807, 0x0808, 0x081a, 0x081b, |
||||
0x081c, 0x0809, 0x080a, 0x080b, 0x0804, 0x0805, 0x0806, 0x0401, 0x0501, 0x0601, 0 |
||||
}; |
||||
|
||||
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
|
||||
}, |
||||
}; |
||||
|
||||
static const char *const alpn[] = {"http/1.1", NULL}; |
||||
static const struct reality_fingerprint profile = {REALITY_FP_OPENSSL, "openssl", suites, groups, signatures, alpn, 0x1301}; |
||||
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; |
||||
return id == REALITY_FP_OPENSSL ? &profile : NULL; |
||||
} |
||||
|
||||
@ -0,0 +1,394 @@
|
||||
#define OPENSSL_API_COMPAT 0x10100000L |
||||
#include "reality_io.h" |
||||
#include "stcp.h" |
||||
#include "reality_relay.h" |
||||
#include "etcp.h" |
||||
#include "../utun_instance.h" |
||||
#include "../ntp_time.h" |
||||
#include "../lib/mem.h" |
||||
#include "../lib/platform_compat.h" |
||||
#include <string.h> |
||||
#include <errno.h> |
||||
#include <time.h> |
||||
#include <openssl/crypto.h> |
||||
#include <openssl/sha.h> |
||||
|
||||
#define REPLAY_CAPACITY 1024 |
||||
enum reality_phase { WAIT_HELLO, WAIT_SERVER_FLIGHT, WAIT_CLIENT_FINISHED, RECORD_DATA }; |
||||
struct reality_replay_cache { |
||||
struct { uint8_t hash[32]; int64_t expires; } entries[REPLAY_CAPACITY]; |
||||
}; |
||||
struct reality_io { |
||||
struct reality_session session; |
||||
enum reality_phase phase; |
||||
struct ETCP_PADDING padding; // снимок для автономного клиента без instance
|
||||
uint8_t *wire, *handshake; |
||||
size_t wire_len, hello_offset, handshake_len; |
||||
uint8_t flight_step, ccs_seen, close_sent; |
||||
void (*ready)(struct stcp_conn *); |
||||
}; |
||||
|
||||
static int replay_accept(struct reality_owner *owner, const uint8_t *ch, size_t len, uint32_t now, |
||||
uint32_t timestamp, int64_t window) { |
||||
if (!owner->replay) owner->replay = u_calloc(1, sizeof(*owner->replay)); |
||||
if (!owner->replay) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "replay cache allocation failed"); return -1; } |
||||
uint8_t hash[32]; |
||||
if (!SHA256(ch, len, hash)) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "replay hash failed"); return -1; } |
||||
int slot = -1; |
||||
for (unsigned i = 0; i < REPLAY_CAPACITY; i++) { |
||||
if (owner->replay->entries[i].expires >= now) { |
||||
if (!CRYPTO_memcmp(hash, owner->replay->entries[i].hash, 32)) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "replayed ClientHello rejected"); |
||||
return -1; |
||||
} |
||||
} else if (slot < 0) slot = (int)i; |
||||
} |
||||
if (slot < 0) { DEBUG_WARN(DEBUG_CATEGORY_REALITY, "replay cache full: capacity=%d", REPLAY_CAPACITY); return -1; } |
||||
memcpy(owner->replay->entries[slot].hash, hash, 32); |
||||
owner->replay->entries[slot].expires = (int64_t)timestamp + window; |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_REALITY, "replay entry stored: slot=%d expires=%lld", slot, |
||||
(long long)owner->replay->entries[slot].expires); |
||||
return 0; |
||||
} |
||||
|
||||
void reality_owner_cleanup(struct reality_owner *owner) { |
||||
reality_relay_shutdown(owner); |
||||
u_free(owner->replay); |
||||
owner->replay = NULL; |
||||
} |
||||
|
||||
void reality_io_cleanup(struct stcp_conn *c) { |
||||
struct reality_io *io = c->reality_io; |
||||
if (!io) return; |
||||
c->reality_io = NULL; |
||||
reality_session_cleanup(&io->session); |
||||
u_free(io->wire); |
||||
u_free(io->handshake); |
||||
u_free(io); |
||||
} |
||||
|
||||
static struct reality_io *io_create(struct stcp_conn *c, void (*ready)(struct stcp_conn *)) { |
||||
struct reality_io *io = u_calloc(1, sizeof(*io)); |
||||
if (!io) goto fail; |
||||
io->wire = u_malloc(STCP_RECV_BUF_MAX); |
||||
io->handshake = u_malloc(REALITY_MAX_CH_SIZE); |
||||
if (!io->wire || !io->handshake) { |
||||
u_free(io->wire); u_free(io->handshake); u_free(io); |
||||
goto fail; |
||||
} |
||||
io->ready = ready; |
||||
c->reality_io = io; |
||||
return io; |
||||
fail: |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "record I/O allocation failed"); |
||||
return NULL; |
||||
} |
||||
|
||||
int reality_io_client_start(struct stcp_conn *c, const struct reality_client_config *cfg, uint32_t now, |
||||
const struct ETCP_PADDING *padding, void (*ready)(struct stcp_conn *)) { |
||||
struct reality_io *io = io_create(c, ready); |
||||
if (!io) return -1; |
||||
if (padding) { io->padding.min = padding->min; io->padding.max = padding->max; } |
||||
uint8_t *ch = u_malloc(REALITY_MAX_CH_SIZE); |
||||
size_t n = 0; |
||||
if (!ch) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "ClientHello buffer allocation failed"); return -1; } |
||||
if (reality_session_client_start(&io->session, cfg, now, ch, REALITY_MAX_CH_SIZE, &n) || stcp_send_raw(c, ch, n) < 0) { |
||||
u_free(ch); |
||||
return -1; |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
int reality_io_server_start(struct stcp_conn *c, void (*ready)(struct stcp_conn *)) { |
||||
struct reality_io *io = io_create(c, ready); |
||||
if (!io) return -1; |
||||
io->session.is_server = 1; |
||||
return 0; |
||||
} |
||||
|
||||
static int send_record(struct stcp_conn *c, uint8_t type, const uint8_t *data, size_t len, size_t padding) { |
||||
uint8_t *wire = u_malloc(5 + len + 1 + padding + 16); |
||||
size_t n = 0; |
||||
if (!wire) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "record tx allocation failed: content=%zu", len); return -1; } |
||||
if (reality_record_seal(&c->reality_io->session.send, type, data, len, padding, wire, |
||||
5 + len + 1 + padding + 16, &n) || stcp_send_raw(c, wire, n) < 0) { |
||||
u_free(wire); |
||||
return -1; |
||||
} |
||||
return c->send_buf ? 1 : 0; |
||||
} |
||||
|
||||
static int send_key_update(struct stcp_conn *c) { |
||||
const uint8_t update[] = {24, 0, 0, 1, 0}; |
||||
if (send_record(c, REALITY_TYPE_HANDSHAKE, update, sizeof(update), 0) < 0) return -1; |
||||
return reality_traffic_update(&c->reality_io->session.send); |
||||
} |
||||
|
||||
int reality_io_send(struct stcp_conn *c, uint8_t *data, size_t len) { |
||||
struct reality_io *io = c->reality_io; |
||||
if (io->phase != RECORD_DATA || io->close_sent) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "application tx before Finished or after close: phase=%d", io->phase); |
||||
return -1; |
||||
} |
||||
const struct ETCP_PADDING *profile = c->etcp_conn ? &c->etcp_conn->padding : |
||||
(c->inst ? etcp_padding_for_peer(c->inst, c->peer_pubkey_set ? c->peer_pubkey : NULL) : &io->padding); |
||||
size_t offset = 0; |
||||
do { |
||||
if (io->session.send.sequence >= REALITY_KEY_LIMIT - 1 && send_key_update(c)) return -1; |
||||
int pad = etcp_padding_size(profile, ETCP_PADDING_MAX); |
||||
if (pad < 0) return -1; |
||||
size_t take = len - offset; |
||||
if (take > REALITY_INNER_MAX - 1 - (size_t)pad) take = REALITY_INNER_MAX - 1 - (size_t)pad; |
||||
if (send_record(c, REALITY_TYPE_APPLICATION, data + offset, take, pad) < 0) return -1; |
||||
offset += take; |
||||
} while (offset < len); |
||||
u_free(data); |
||||
return c->send_buf ? 1 : 0; |
||||
} |
||||
|
||||
int reality_io_close_notify(struct stcp_conn *c) { |
||||
struct reality_io *io = c->reality_io; |
||||
if (io->phase != RECORD_DATA || io->close_sent) return 0; |
||||
io->close_sent = 1; |
||||
const uint8_t alert[] = {1, 0}; |
||||
if (io->session.send.sequence >= REALITY_KEY_LIMIT - 1 && send_key_update(c)) return -1; |
||||
int rc = send_record(c, REALITY_TYPE_ALERT, alert, sizeof(alert), 0); |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_REALITY, "close_notify queued: pending=%d", rc); |
||||
return rc; |
||||
} |
||||
|
||||
static int send_ccs(struct stcp_conn *c) { |
||||
const uint8_t ccs[] = {20, 3, 3, 0, 1, 1}; |
||||
uint8_t *wire = u_malloc(sizeof(ccs)); |
||||
if (!wire) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "CCS allocation failed"); return -1; } |
||||
memcpy(wire, ccs, sizeof(ccs)); |
||||
if (stcp_send_raw(c, wire, sizeof(ccs)) < 0) { u_free(wire); return -1; } |
||||
return 0; |
||||
} |
||||
|
||||
static int send_finished(struct stcp_conn *c) { |
||||
uint8_t finished[36] = {20, 0, 0, 32}; |
||||
struct reality_session *s = &c->reality_io->session; |
||||
if (!s->is_server && send_ccs(c)) return -1; |
||||
if (reality_finished(s, 1, finished + 4) || send_record(c, REALITY_TYPE_HANDSHAKE, finished, 36, 0) < 0 || |
||||
reality_transcript_add(s, finished, sizeof(finished))) return -1; |
||||
return 0; |
||||
} |
||||
|
||||
static int send_server_flight(struct stcp_conn *c) { |
||||
/* Размеры записей эталонного single-certificate RSA flight OpenSSL: 43/832/286/58.
|
||||
* Cover-данные не используются как PKI; их точные байты включаются в Finished. */ |
||||
const uint8_t ee[] = {8, 0, 0, 17, 0, 15, 0, 16, 0, 11, 0, 9, 8, 'h', 't', 't', 'p', '/', '1', '.', '1'}; |
||||
uint8_t certificate[810], verify[264]; |
||||
if (random_bytes(certificate, sizeof(certificate)) || random_bytes(verify, sizeof(verify))) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "cover flight random failed"); |
||||
return -1; |
||||
} |
||||
certificate[0] = 11; certificate[1] = 0; certificate[2] = 3; certificate[3] = 38; |
||||
verify[0] = 15; verify[1] = 0; verify[2] = 1; verify[3] = 4; |
||||
verify[4] = 8; verify[5] = 4; verify[6] = 1; verify[7] = 0; |
||||
struct reality_session *session = &c->reality_io->session; |
||||
if (send_ccs(c)) return -1; |
||||
if (reality_transcript_add(session, ee, sizeof(ee)) || send_record(c, REALITY_TYPE_HANDSHAKE, ee, sizeof(ee), 0) < 0 || |
||||
reality_transcript_add(session, certificate, sizeof(certificate)) || |
||||
send_record(c, REALITY_TYPE_HANDSHAKE, certificate, sizeof(certificate), 0) < 0 || |
||||
reality_transcript_add(session, verify, sizeof(verify)) || send_record(c, REALITY_TYPE_HANDSHAKE, verify, sizeof(verify), 0) < 0 || |
||||
send_finished(c)) return -1; |
||||
return 0; |
||||
} |
||||
|
||||
static int fallback(struct stcp_conn *c) { |
||||
struct reality_io *io = c->reality_io; |
||||
if (!io->session.is_server || io->phase != WAIT_HELLO) return -1; |
||||
socket_t sock = c->sock; |
||||
c->sock = SOCKET_INVALID; |
||||
c->socket_id = NULL; |
||||
struct reality_owner *owner = c->reality_owner; |
||||
reality_relay_start(owner, c->ua, sock, c->reality_dest, io->wire, io->wire_len, c->reality_srv.relay_idle_timeout_sec); |
||||
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "unauthorized connection transferred to relay: buffered=%zu", io->wire_len); |
||||
stcp_conn_do_close(c, ECANCELED); |
||||
return -1; |
||||
} |
||||
|
||||
static int hello_process(struct stcp_conn *c) { |
||||
struct reality_io *io = c->reality_io; |
||||
while (io->wire_len - io->hello_offset >= 5) { |
||||
const uint8_t *record = io->wire + io->hello_offset; |
||||
size_t n = ((size_t)record[3] << 8) | record[4]; |
||||
if (record[0] != 22 || record[1] != 3 || (record[2] != 3 && (!io->session.is_server || record[2] != 1)) || !n || n > 16384 || |
||||
io->handshake_len + n > REALITY_MAX_CH_SIZE - 5) goto reject; |
||||
if (io->wire_len - io->hello_offset < n + 5) return 0; |
||||
memcpy(io->handshake + io->handshake_len, record + 5, n); |
||||
io->handshake_len += n; |
||||
io->hello_offset += n + 5; |
||||
if (io->handshake_len < 4) continue; |
||||
size_t want = 4 + ((size_t)io->handshake[1] << 16) + ((size_t)io->handshake[2] << 8) + io->handshake[3]; |
||||
if (want > REALITY_MAX_CH_SIZE - 5 || io->handshake_len > want) goto reject; |
||||
if (io->handshake_len < want) continue; |
||||
uint8_t *full = u_malloc(want + 5); |
||||
if (!full) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "Hello normalization allocation failed"); return -1; } |
||||
full[0] = 22; full[1] = 3; full[2] = io->session.is_server ? 1 : 3; |
||||
full[3] = (uint8_t)(want >> 8); full[4] = (uint8_t)want; |
||||
memcpy(full + 5, io->handshake, want); |
||||
int rc; |
||||
if (io->session.is_server) { |
||||
uint8_t *sh = u_malloc(REALITY_MAX_SH_SIZE); |
||||
size_t written = 0; |
||||
uint32_t now = c->inst ? (uint32_t)ntp_time_get_seconds(c->inst) : (uint32_t)time(NULL); |
||||
if (!sh) DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "ServerHello output allocation failed"); |
||||
rc = sh ? reality_session_server_start(&io->session, &c->reality_srv, now, full, want + 5, |
||||
sh, REALITY_MAX_SH_SIZE, &written) : REALITY_ERR_CRYPTO; |
||||
if (!rc && (!c->reality_owner || replay_accept(c->reality_owner, io->handshake, want, now, |
||||
io->session.auth_timestamp, c->reality_srv.time_window_sec))) rc = REALITY_ERR_AUTH; |
||||
if (rc) { u_free(sh); u_free(full); goto reject; } |
||||
io->phase = WAIT_CLIENT_FINISHED; // fallback запрещён с момента постановки собственного ответа
|
||||
if (stcp_send_raw(c, sh, written) < 0) { u_free(sh); u_free(full); return -1; } |
||||
if (send_server_flight(c)) { u_free(full); return -1; } |
||||
} else { |
||||
rc = reality_session_client_accept(&io->session, full, want + 5); |
||||
if (rc) { u_free(full); return -1; } |
||||
io->phase = WAIT_SERVER_FLIGHT; |
||||
} |
||||
u_free(full); |
||||
memmove(io->wire, io->wire + io->hello_offset, io->wire_len - io->hello_offset); |
||||
io->wire_len -= io->hello_offset; |
||||
io->hello_offset = 0; |
||||
io->handshake_len = 0; |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_REALITY, "Hello complete: role=%s phase=%d", io->session.is_server ? "server" : "client", io->phase); |
||||
return 1; |
||||
} |
||||
return 0; |
||||
reject: |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "Hello rejected: buffered=%zu handshake=%zu", io->wire_len, io->handshake_len); |
||||
return fallback(c); |
||||
} |
||||
|
||||
static int handshake_process(struct stcp_conn *c, const uint8_t *data, size_t len) { |
||||
struct reality_io *io = c->reality_io; |
||||
if (len > REALITY_MAX_CH_SIZE - io->handshake_len) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "protected handshake too large"); |
||||
return -1; |
||||
} |
||||
memcpy(io->handshake + io->handshake_len, data, len); |
||||
io->handshake_len += len; |
||||
while (io->handshake_len >= 4) { |
||||
uint8_t type = io->handshake[0]; |
||||
size_t n = 4 + ((size_t)io->handshake[1] << 16) + ((size_t)io->handshake[2] << 8) + io->handshake[3]; |
||||
if (n > REALITY_MAX_CH_SIZE) { DEBUG_WARN(DEBUG_CATEGORY_REALITY, "protected handshake length rejected: %zu", n); return -1; } |
||||
if (n > io->handshake_len) return 0; |
||||
if (io->phase == RECORD_DATA) { |
||||
if (n != 5 || io->handshake_len != 5 || type != 24 || io->handshake[4] != 0 || reality_traffic_update(&io->session.recv)) goto reject; |
||||
} else if (type == 20) { |
||||
uint8_t expected[32]; |
||||
if (n != 36 || (io->phase == WAIT_SERVER_FLIGHT && io->flight_step != 3) || |
||||
reality_finished(&io->session, 0, expected) || CRYPTO_memcmp(expected, io->handshake + 4, 32) || |
||||
reality_transcript_add(&io->session, io->handshake, n)) goto reject; |
||||
if (io->phase == WAIT_SERVER_FLIGHT && send_finished(c)) return -1; |
||||
io->phase = RECORD_DATA; |
||||
reality_session_handshake_done(&io->session); |
||||
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "Finished verified: role=%s; protected STCP enabled", io->session.is_server ? "server" : "client"); |
||||
io->ready(c); |
||||
if (!c->reality_io || c->state == STCP_STATE_CLOSED || c->state == STCP_STATE_ERROR) return -1; |
||||
} else { |
||||
const uint8_t steps[] = {8, 11, 15}; |
||||
if (io->phase != WAIT_SERVER_FLIGHT || io->flight_step >= 3 || type != steps[io->flight_step] || |
||||
reality_transcript_add(&io->session, io->handshake, n)) goto reject; |
||||
io->flight_step++; |
||||
} |
||||
memmove(io->handshake, io->handshake + n, io->handshake_len - n); |
||||
io->handshake_len -= n; |
||||
} |
||||
return 0; |
||||
reject: |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "protected handshake rejected: phase=%d step=%u type=%u size=%zu", io->phase, |
||||
io->flight_step, io->handshake[0], io->handshake_len); |
||||
return -1; |
||||
} |
||||
|
||||
static int append_application(struct stcp_conn *c, const uint8_t *data, size_t len) { |
||||
if (len > STCP_RECV_BUF_MAX - c->recv_buf_len) { DEBUG_WARN(DEBUG_CATEGORY_REALITY, "STCP plaintext buffer full"); return -1; } |
||||
if (c->recv_buf_len + len > c->recv_buf_cap) { |
||||
size_t cap = c->recv_buf_len + len; |
||||
if (cap < STCP_RECV_BUF_INIT) cap = STCP_RECV_BUF_INIT; |
||||
uint8_t *p = u_realloc(c->recv_buf, cap); |
||||
if (!p) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "STCP plaintext buffer allocation failed: %zu", cap); return -1; } |
||||
c->recv_buf = p; c->recv_buf_cap = cap; |
||||
} |
||||
if (len) memcpy(c->recv_buf + c->recv_buf_len, data, len); |
||||
c->recv_buf_len += len; |
||||
stcp_recv_try(c); |
||||
return c->reality_io ? 0 : -1; |
||||
} |
||||
|
||||
static int records_process(struct stcp_conn *c) { |
||||
struct reality_io *io = c->reality_io; |
||||
if (io->phase == WAIT_HELLO) { |
||||
int rc = hello_process(c); |
||||
if (rc <= 0) return rc; |
||||
} |
||||
while (io->wire_len >= 5 && !c->rx_paused && !c->finish_requested) { |
||||
size_t n = 5 + ((size_t)io->wire[3] << 8) + io->wire[4]; |
||||
if (n > REALITY_RECORD_MAX || n < 6) goto reject; |
||||
if (io->wire_len < n) return 0; |
||||
if (io->wire[0] == 20) { |
||||
const uint8_t ccs[] = {20, 3, 3, 0, 1, 1}; |
||||
if (io->phase == RECORD_DATA || io->ccs_seen || n != sizeof(ccs) || memcmp(io->wire, ccs, sizeof(ccs))) goto reject; |
||||
io->ccs_seen = 1; |
||||
} else { |
||||
uint8_t plain[REALITY_INNER_MAX], type; |
||||
size_t len = 0; |
||||
if (reality_record_open(&io->session.recv, io->wire, n, plain, sizeof(plain), &len, &type)) goto reject; |
||||
if (type == REALITY_TYPE_HANDSHAKE) { |
||||
if (handshake_process(c, plain, len)) return -1; |
||||
} else if (type == REALITY_TYPE_APPLICATION) { |
||||
if (io->phase != RECORD_DATA || io->handshake_len) goto reject; |
||||
if (append_application(c, plain, len)) return -1; |
||||
} else { |
||||
if (io->phase != RECORD_DATA || len != 2 || plain[0] != 1 || plain[1] != 0) goto reject; |
||||
stcp_conn_finish(c); |
||||
return -2; |
||||
} |
||||
if (!c->reality_io) return -1; |
||||
} |
||||
memmove(io->wire, io->wire + n, io->wire_len - n); |
||||
io->wire_len -= n; |
||||
} |
||||
return 0; |
||||
reject: |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "record stream rejected: phase=%d buffered=%zu", io->phase, io->wire_len); |
||||
return -1; |
||||
} |
||||
|
||||
void reality_io_resume(struct stcp_conn *c) { |
||||
if (c->reality_io && records_process(c) < 0) stcp_conn_do_close(c, ECANCELED); |
||||
} |
||||
|
||||
size_t reality_io_buffered(const struct stcp_conn *c) { |
||||
return c && c->reality_io ? c->reality_io->wire_len : 0; |
||||
} |
||||
|
||||
int reality_io_read(struct stcp_conn *c) { |
||||
struct reality_io *io = c->reality_io; |
||||
if (io->wire_len == STCP_RECV_BUF_MAX) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "record receive buffer limit exceeded"); |
||||
stcp_conn_do_close(c, ENOBUFS); |
||||
return -1; |
||||
} |
||||
ssize_t n = recv(c->sock, io->wire + io->wire_len, STCP_RECV_BUF_MAX - io->wire_len, 0); |
||||
if (n < 0) { |
||||
int err = socket_get_error(); |
||||
if (err == ERR_AGAIN || err == ERR_WOULDBLOCK) return 0; |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "record socket read failed: err=%d", err); |
||||
stcp_conn_do_close(c, err); |
||||
return -1; |
||||
} |
||||
if (!n) { |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_REALITY, "record socket EOF: phase=%d incomplete=%zu", io->phase, io->wire_len); |
||||
stcp_conn_do_close(c, io->wire_len || io->phase != RECORD_DATA ? ECANCELED : 0); |
||||
return -1; |
||||
} |
||||
io->wire_len += (size_t)n; |
||||
int rc = records_process(c); |
||||
if (rc < 0) { if (rc == -1) stcp_conn_do_close(c, ECANCELED); return -1; } |
||||
return 1; |
||||
} |
||||
@ -0,0 +1,39 @@
|
||||
/* Поток записей отделён от внутреннего буфера STCP; все вызовы — в uasync-потоке владельца.
|
||||
* CH -> SH + CCS + encrypted(EE/cover Certificate/cover CertificateVerify/Server Finished) |
||||
* -> CCS + encrypted(Client Finished) -> encrypted(STCP handshake и последующий поток). |
||||
* Проверка стороны использует Finished, cover Certificate/CertificateVerify не являются PKI. |
||||
* Root = HKDF-SHA256(AuthKey || X25519(client_ephemeral, server_ephemeral), SHA256(CH || SH), |
||||
* "uTun-reality-record-v2"); в transcript входят handshake-заголовки, но не record-заголовки. |
||||
* От root раздельно выводятся client-traffic/server-traffic и их key/iv/finished-ключи. |
||||
* После собственного SH fallback запрещён. До него relay получает весь исходный буфер без нормализации. |
||||
* Таймеры/списки/кеш replay принадлежат instance; destroy принудительно закрывает I/O без ожидания callbacks. */ |
||||
#ifndef REALITY_IO_H |
||||
#define REALITY_IO_H |
||||
#include "reality.h" |
||||
#include "etcp_padding.h" |
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
struct stcp_conn; |
||||
struct reality_io; |
||||
struct reality_replay_cache; |
||||
struct reality_relay; |
||||
struct reality_owner { |
||||
struct reality_replay_cache *replay; |
||||
struct reality_relay *relays; |
||||
unsigned relay_count; |
||||
}; |
||||
int reality_io_client_start(struct stcp_conn *c, const struct reality_client_config *cfg, uint32_t now, |
||||
const struct ETCP_PADDING *padding, void (*ready)(struct stcp_conn *)); |
||||
int reality_io_server_start(struct stcp_conn *c, void (*ready)(struct stcp_conn *)); |
||||
size_t reality_io_buffered(const struct stcp_conn *c); |
||||
int reality_io_read(struct stcp_conn *c); |
||||
void reality_io_resume(struct stcp_conn *c); |
||||
int reality_io_send(struct stcp_conn *c, uint8_t *data, size_t len); |
||||
int reality_io_close_notify(struct stcp_conn *c); |
||||
void reality_io_cleanup(struct stcp_conn *c); |
||||
void reality_owner_cleanup(struct reality_owner *owner); |
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif |
||||
@ -0,0 +1,128 @@
|
||||
#define OPENSSL_API_COMPAT 0x10100000L |
||||
#include "reality_record.h" |
||||
#include "../lib/debug_config.h" |
||||
#include <string.h> |
||||
#include <limits.h> |
||||
#include <openssl/evp.h> |
||||
#include <openssl/kdf.h> |
||||
#include <openssl/crypto.h> |
||||
|
||||
int reality_kdf(const uint8_t *ikm, size_t ikm_len, const uint8_t *salt, size_t salt_len, |
||||
const char *label, uint8_t *out, size_t out_len) { |
||||
if ((!ikm && ikm_len) || (!salt && salt_len) || !label || !out || !out_len || ikm_len > INT_MAX || salt_len > INT_MAX) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "invalid KDF arguments"); |
||||
return -1; |
||||
} |
||||
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL); |
||||
size_t n = out_len; |
||||
int ok = ctx && EVP_PKEY_derive_init(ctx) > 0 && EVP_PKEY_CTX_set_hkdf_md(ctx, EVP_sha256()) > 0 && |
||||
(!salt_len || EVP_PKEY_CTX_set1_hkdf_salt(ctx, salt, (int)salt_len) > 0) && |
||||
EVP_PKEY_CTX_set1_hkdf_key(ctx, ikm, (int)ikm_len) > 0 && |
||||
EVP_PKEY_CTX_add1_hkdf_info(ctx, (const uint8_t *)label, (int)strlen(label)) > 0 && |
||||
EVP_PKEY_derive(ctx, out, &n) > 0 && n == out_len; |
||||
EVP_PKEY_CTX_free(ctx); |
||||
if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "record KDF failed: label=%s", label); |
||||
return ok ? 0 : -1; |
||||
} |
||||
|
||||
int reality_traffic_init(struct reality_traffic *t, const uint8_t secret[32]) { |
||||
if (!t || !secret) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "invalid traffic key arguments"); return -1; } |
||||
memcpy(t->secret, secret, 32); |
||||
t->sequence = 0; |
||||
if (reality_kdf(secret, 32, NULL, 0, "uTun-reality-key", t->key, 16) || |
||||
reality_kdf(secret, 32, NULL, 0, "uTun-reality-iv", t->iv, 12)) return -1; |
||||
return 0; |
||||
} |
||||
|
||||
int reality_traffic_update(struct reality_traffic *t) { |
||||
if (!t) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "missing traffic state on KeyUpdate"); return -1; } |
||||
uint8_t next[32]; |
||||
int rc = reality_kdf(t->secret, 32, NULL, 0, "uTun-reality-update", next, 32); |
||||
if (!rc) rc = reality_traffic_init(t, next); |
||||
OPENSSL_cleanse(next, sizeof(next)); |
||||
if (rc) return -1; |
||||
t->generation++; |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_REALITY, "record KeyUpdate accepted: generation=%u", t->generation); |
||||
return 0; |
||||
} |
||||
|
||||
static void record_nonce(const struct reality_traffic *t, uint8_t nonce[12]) { |
||||
memcpy(nonce, t->iv, 12); |
||||
for (unsigned i = 0; i < 8; i++) nonce[11 - i] ^= (uint8_t)(t->sequence >> (i * 8)); |
||||
} |
||||
|
||||
static int valid_type(uint8_t type) { |
||||
return type == REALITY_TYPE_HANDSHAKE || type == REALITY_TYPE_APPLICATION || type == REALITY_TYPE_ALERT; |
||||
} |
||||
|
||||
int reality_record_seal(struct reality_traffic *t, uint8_t type, const uint8_t *data, size_t len, |
||||
size_t padding, uint8_t *out, size_t cap, size_t *written) { |
||||
if (!t || !out || !written || !valid_type(type) || (!data && len) || |
||||
len > REALITY_INNER_MAX - 1 || padding > REALITY_INNER_MAX - 1 - len || |
||||
cap < 5 + len + 1 + padding + 16 || t->sequence >= REALITY_KEY_LIMIT || |
||||
(!len && type != REALITY_TYPE_APPLICATION)) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "record seal rejected: type=%u content=%zu padding=%zu", type, len, padding); |
||||
return -1; |
||||
} |
||||
uint8_t plain[REALITY_INNER_MAX], nonce[12]; |
||||
size_t n = len + 1 + padding, cipher_len = n + 16; |
||||
if (len) memcpy(plain, data, len); |
||||
plain[len] = type; |
||||
memset(plain + len + 1, 0, padding); |
||||
out[0] = 23; out[1] = 3; out[2] = 3; out[3] = (uint8_t)(cipher_len >> 8); out[4] = (uint8_t)cipher_len; |
||||
record_nonce(t, nonce); |
||||
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); |
||||
int size = 0, final = 0; |
||||
int ok = ctx && EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), NULL, t->key, nonce) == 1 && |
||||
EVP_EncryptUpdate(ctx, NULL, &size, out, 5) == 1 && |
||||
EVP_EncryptUpdate(ctx, out + 5, &size, plain, (int)n) == 1 && (size_t)size == n && |
||||
EVP_EncryptFinal_ex(ctx, out + 5 + size, &final) == 1 && final == 0 && |
||||
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, 16, out + 5 + n) == 1; |
||||
EVP_CIPHER_CTX_free(ctx); |
||||
OPENSSL_cleanse(plain, n); |
||||
if (!ok) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "record encryption failed: seq=%llu", (unsigned long long)t->sequence); return -1; } |
||||
DEBUG_TRACE(DEBUG_CATEGORY_REALITY, "record tx: generation=%u seq=%llu type=%u content=%zu padding=%zu", |
||||
t->generation, (unsigned long long)t->sequence, type, len, padding); |
||||
t->sequence++; |
||||
*written = 5 + cipher_len; |
||||
return 0; |
||||
} |
||||
|
||||
int reality_record_open(struct reality_traffic *t, const uint8_t *record, size_t len, |
||||
uint8_t *out, size_t cap, size_t *written, uint8_t *type) { |
||||
if (!t || !record || !out || !written || !type || len < 22 || len > REALITY_RECORD_MAX || |
||||
record[0] != 23 || record[1] != 3 || record[2] != 3 || |
||||
(((size_t)record[3] << 8) | record[4]) != len - 5 || cap < len - 21 || t->sequence >= REALITY_KEY_LIMIT) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "record header rejected: wire=%zu", len); |
||||
return -1; |
||||
} |
||||
uint8_t nonce[12]; |
||||
record_nonce(t, nonce); |
||||
size_t n = len - 21; |
||||
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); |
||||
int size = 0, final = 0; |
||||
int ok = ctx && EVP_DecryptInit_ex(ctx, EVP_aes_128_gcm(), NULL, t->key, nonce) == 1 && |
||||
EVP_DecryptUpdate(ctx, NULL, &size, record, 5) == 1 && |
||||
EVP_DecryptUpdate(ctx, out, &size, record + 5, (int)n) == 1 && (size_t)size == n && |
||||
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, 16, (void *)(record + 5 + n)) == 1 && |
||||
EVP_DecryptFinal_ex(ctx, out + size, &final) == 1; |
||||
EVP_CIPHER_CTX_free(ctx); |
||||
if (!ok) { |
||||
OPENSSL_cleanse(out, n); |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "record authentication failed: generation=%u seq=%llu", t->generation, |
||||
(unsigned long long)t->sequence); |
||||
return -1; |
||||
} |
||||
while (n && !out[n - 1]) n--; |
||||
if (!n || !valid_type(out[n - 1]) || (n == 1 && out[0] != REALITY_TYPE_APPLICATION)) { |
||||
OPENSSL_cleanse(out, len - 21); |
||||
DEBUG_WARN(DEBUG_CATEGORY_REALITY, "record inner type rejected"); |
||||
return -1; |
||||
} |
||||
*type = out[--n]; |
||||
*written = n; |
||||
DEBUG_TRACE(DEBUG_CATEGORY_REALITY, "record rx: generation=%u seq=%llu type=%u content=%zu", t->generation, |
||||
(unsigned long long)t->sequence, *type, n); |
||||
t->sequence++; |
||||
return 0; |
||||
} |
||||
@ -0,0 +1,41 @@
|
||||
/* AEAD-записи внешнего REALITY; состояние обнуляется при создании и принадлежит одному TCP-сеансу.
|
||||
* Wire: 17 03 03 | uint16_be(ciphertext_len) | AES-128-GCM(content || inner_type || zero_padding) | tag[16]. |
||||
* AAD — пять байт заголовка; nonce[12] = base_iv XOR (zero[4] || uint64_be(sequence)). |
||||
* Каждый успешный seal/open увеличивает свой sequence; повторно передаются только готовые wire-байты. |
||||
* Ошибка требует закрытия сеанса. KeyUpdate последним сообщением под старым ключом меняет traffic secret |
||||
* через HKDF("uTun-reality-update"), затем key/iv и sequence=0. Направления обновляются независимо. */ |
||||
#ifndef REALITY_RECORD_H |
||||
#define REALITY_RECORD_H |
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
#define REALITY_INNER_MAX 16385 |
||||
#define REALITY_RECORD_MAX (5 + REALITY_INNER_MAX + 16) |
||||
#ifndef REALITY_KEY_LIMIT |
||||
#define REALITY_KEY_LIMIT (UINT64_C(1) << 20) |
||||
#endif |
||||
#define REALITY_TYPE_HANDSHAKE 22 |
||||
#define REALITY_TYPE_APPLICATION 23 |
||||
#define REALITY_TYPE_ALERT 21 |
||||
|
||||
struct reality_traffic { |
||||
uint8_t secret[32], key[16], iv[12]; |
||||
uint64_t sequence; |
||||
uint32_t generation; |
||||
}; |
||||
|
||||
int reality_kdf(const uint8_t *ikm, size_t ikm_len, const uint8_t *salt, size_t salt_len, |
||||
const char *label, uint8_t *out, size_t out_len); |
||||
int reality_traffic_init(struct reality_traffic *t, const uint8_t secret[32]); |
||||
int reality_traffic_update(struct reality_traffic *t); |
||||
int reality_record_seal(struct reality_traffic *t, uint8_t type, const uint8_t *data, size_t len, |
||||
size_t padding, uint8_t *out, size_t cap, size_t *written); |
||||
int reality_record_open(struct reality_traffic *t, const uint8_t *record, size_t len, |
||||
uint8_t *out, size_t cap, size_t *written, uint8_t *type); |
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif |
||||
@ -0,0 +1,3 @@
|
||||
# OpenSSL 3.5.5, 27 Jan 2026; local capture 2026-10-07. |
||||
# openssl s_client -tls1_3 -groups X25519 -ciphersuites TLS_AES_128_GCM_SHA256 -alpn http/1.1 -servername www.microsoft.com -connect 127.0.0.1:PORT |
||||
1603010102010000fe0303495c3aad4d7fbc7f0ea12245795f052c33c7700a56c9f15bd58374992a75f617204f5583134eaae74da0bd4cf1ed233297a41f0ff7bd0118d3b0c418098ca5542e00021301010000b30000001600140000117777772e6d6963726f736f66742e636f6d000b000403000102000a00040002001d002300000010000b000908687474702f312e310016000000170000000d002a002809050906090404030503060308070808081a081b081c0809080a080b080408050806040105010601002b0003020304002d00020101003300260024001d002016a64663eb5b32977d0fbbe63eea3498ee62dd67428c7c25d26ac567b4d06904001b00050400010003 |
||||
@ -0,0 +1,112 @@
|
||||
/* Реальный relay: нормализация replay, сохранение хвоста, независимое владение instance. */ |
||||
#include "stcp_server.h" |
||||
#include "utun_instance.h" |
||||
#include "../lib/mem.h" |
||||
#include "../lib/debug_config.h" |
||||
#include <string.h> |
||||
#include <time.h> |
||||
#include <errno.h> |
||||
|
||||
#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "FAIL line=%d: %s", __LINE__, #x); return 1; } } while (0) |
||||
static uint32_t fake_now; |
||||
time_t __wrap_ntp_time_get_seconds(struct UTUN_INSTANCE *inst) { (void)inst; return fake_now; } |
||||
struct destination_peer { socket_t sock; struct UASYNC *ua; uint8_t data[4096]; size_t len; }; |
||||
struct destination { socket_t sock; struct UASYNC *ua; unsigned count; struct destination_peer peers[4]; }; |
||||
|
||||
static void destination_read(socket_t sock, void *arg) { |
||||
struct destination_peer *p = arg; |
||||
ssize_t n = recv(sock, p->data + p->len, sizeof(p->data) - p->len, 0); |
||||
if (n > 0) p->len += (size_t)n; |
||||
else if (!n) { uasync_remove_socket_t(p->ua, sock); socket_close_wrapper(sock); p->sock = SOCKET_INVALID; } |
||||
} |
||||
static void destination_accept(socket_t sock, void *arg) { |
||||
struct destination *d = arg; |
||||
socket_t peer = accept(sock, NULL, NULL); |
||||
if (peer == SOCKET_INVALID || d->count >= 4) return; |
||||
struct destination_peer *p = &d->peers[d->count++]; |
||||
p->sock = peer; p->ua = d->ua; |
||||
socket_set_nonblocking(peer); |
||||
uasync_add_socket_t(d->ua, peer, destination_read, NULL, NULL, "reality_test_dest", p); |
||||
} |
||||
static void unexpected_stcp(struct stcp_conn *c, void *arg) { |
||||
(void)c; (void)arg; |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "unexpected STCP before Client Finished"); |
||||
} |
||||
static socket_t connect_to(uint16_t port) { |
||||
socket_t s = socket(AF_INET, SOCK_STREAM, 0); |
||||
struct sockaddr_in address = {0}; |
||||
address.sin_family = AF_INET; address.sin_port = htons(port); address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
||||
if (connect(s, (struct sockaddr *)&address, sizeof(address))) { socket_close_wrapper(s); return SOCKET_INVALID; } |
||||
socket_set_nonblocking(s); |
||||
return s; |
||||
} |
||||
static void poll_some(struct UASYNC *ua) { for (unsigned i = 0; i < 100; i++) uasync_poll(ua, 0); } |
||||
|
||||
int main(void) { |
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_DEBUG); debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_WARN); |
||||
size_t allocated = u_get_allocated_count(); |
||||
struct UASYNC *ua = uasync_create(); CHECK(ua); |
||||
struct UTUN_INSTANCE a = {0}, b = {0}; a.ua = b.ua = ua; |
||||
struct SC_MYKEYS keys; CHECK(sc_generate_keypair(&keys) == SC_OK); |
||||
struct reality_config cfg; reality_config_set_defaults(&cfg); |
||||
struct reality_client_config cc; reality_client_config_set_defaults(&cc); |
||||
cfg.enabled = 1; cfg.short_id_count = 1; |
||||
CHECK(!reality_generate_keypair(cfg.private_key, cc.server_static_pubkey)); |
||||
strcpy(cc.server_name, "www.microsoft.com"); |
||||
struct destination dest = {0}; dest.ua = ua; |
||||
dest.sock = socket(AF_INET, SOCK_STREAM, 0); CHECK(dest.sock != SOCKET_INVALID); |
||||
struct sockaddr_in address = {0}; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
||||
CHECK(!bind(dest.sock, (struct sockaddr *)&address, sizeof(address)) && !listen(dest.sock, 4)); |
||||
socklen_t size = sizeof(address); CHECK(!getsockname(dest.sock, (struct sockaddr *)&address, &size)); |
||||
socket_set_nonblocking(dest.sock); |
||||
CHECK(uasync_add_socket_t(ua, dest.sock, destination_accept, NULL, NULL, "reality_test_listener", &dest)); |
||||
snprintf(cfg.dest, sizeof(cfg.dest), "127.0.0.1:%u", ntohs(address.sin_port)); |
||||
uint16_t port = 32000 + (uint16_t)(time(NULL) % 1000); |
||||
struct stcp_server *sa = stcp_server_create(ua, port, &keys, NULL, &a, unexpected_stcp, NULL, NULL, NULL, AF_INET); |
||||
struct stcp_server *sb = stcp_server_create(ua, port + 1, &keys, NULL, &b, unexpected_stcp, NULL, NULL, NULL, AF_INET); |
||||
CHECK(sa && sb); stcp_server_set_reality(sa, &cfg); stcp_server_set_reality(sb, &cfg); |
||||
uint8_t ch[REALITY_MAX_CH_SIZE], answer[4096]; size_t cn; |
||||
fake_now = (uint32_t)time(NULL); |
||||
CHECK(!reality_client_hello_build_at(&cc, fake_now + 30, ch, sizeof(ch), &cn)); |
||||
socket_t first = connect_to(port); CHECK(first != SOCKET_INVALID && send(first, ch, cn, 0) == (ssize_t)cn); |
||||
poll_some(ua); |
||||
CHECK(recv(first, answer, sizeof(answer), 0) >= 127 && answer[0] == 22); |
||||
/* Через 31 секунду тот же future timestamp ещё допустим: replay обязан оставаться в кеше. */ |
||||
fake_now += 31; |
||||
uint8_t split[4096]; size_t first_part = 11, rest = cn - 5 - first_part; |
||||
memcpy(split, ch, 5); split[3] = 0; split[4] = first_part; |
||||
memcpy(split + 5, ch + 5, first_part); |
||||
size_t pos = 5 + first_part; |
||||
memcpy(split + pos, ch, 5); split[pos + 2] = 3; split[pos + 3] = (uint8_t)(rest >> 8); split[pos + 4] = (uint8_t)rest; |
||||
memcpy(split + pos + 5, ch + 5 + first_part, rest); |
||||
pos += 5 + rest; |
||||
const uint8_t tail[] = {20, 3, 3, 0, 1, 1, 23, 3, 3, 0, 4, 1, 2, 3, 4}; |
||||
memcpy(split + pos, tail, sizeof(tail)); pos += sizeof(tail); |
||||
socket_t replay = connect_to(port); CHECK(replay != SOCKET_INVALID); |
||||
CHECK(send(replay, split, 7, 0) == 7); poll_some(ua); |
||||
CHECK(send(replay, split + 7, pos - 7, 0) == (ssize_t)(pos - 7)); poll_some(ua); |
||||
CHECK(dest.count == 1 && dest.peers[0].len == pos && !memcmp(dest.peers[0].data, split, pos)); |
||||
CHECK(a.reality_owner.relay_count == 1); |
||||
socket_t other = connect_to(port + 1); CHECK(other != SOCKET_INVALID && send(other, ch, cn, 0) == (ssize_t)cn); |
||||
poll_some(ua); |
||||
CHECK(recv(other, answer, sizeof(answer), 0) >= 127 && answer[0] == 22 && dest.count == 1); |
||||
|
||||
cc.short_id[0] = 1; |
||||
CHECK(!reality_client_hello_build_at(&cc, fake_now, ch, sizeof(ch), &cn)); |
||||
socket_t unauthorized = connect_to(port + 1); |
||||
CHECK(unauthorized != SOCKET_INVALID && send(unauthorized, ch, cn, 0) == (ssize_t)cn); |
||||
poll_some(ua); CHECK(dest.count == 2 && b.reality_owner.relay_count == 1); |
||||
stcp_server_destroy(sb); reality_owner_cleanup(&b.reality_owner); poll_some(ua); |
||||
CHECK(a.reality_owner.relay_count == 1 && dest.peers[1].sock == SOCKET_INVALID); |
||||
CHECK(send(replay, tail, sizeof(tail), 0) == sizeof(tail)); poll_some(ua); |
||||
CHECK(dest.peers[0].len == pos + sizeof(tail) && !memcmp(dest.peers[0].data + pos, tail, sizeof(tail))); |
||||
stcp_server_destroy(sa); reality_owner_cleanup(&a.reality_owner); poll_some(ua); |
||||
socket_close_wrapper(first); socket_close_wrapper(replay); socket_close_wrapper(other); socket_close_wrapper(unauthorized); |
||||
for (unsigned i = 0; i < dest.count; i++) if (dest.peers[i].sock != SOCKET_INVALID) { |
||||
uasync_remove_socket_t(ua, dest.peers[i].sock); socket_close_wrapper(dest.peers[i].sock); |
||||
} |
||||
uasync_remove_socket_t(ua, dest.sock); socket_close_wrapper(dest.sock); uasync_destroy(ua, 1); |
||||
CHECK(u_get_allocated_count() == allocated); |
||||
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "fragmented replay, future expiry, exact fallback bytes, instance isolation and teardown passed"); |
||||
return 0; |
||||
} |
||||
@ -0,0 +1,118 @@
|
||||
/* Независимые HKDF/GCM-векторы и проверка привязки записей к обоим Hello. */ |
||||
#include "reality.h" |
||||
#include "../lib/debug_config.h" |
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <openssl/crypto.h> |
||||
|
||||
#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "FAIL line=%d: %s", __LINE__, #x); return 1; } } while (0) |
||||
static size_t unhex(const char *s, uint8_t *out) { |
||||
size_t n = 0; |
||||
while (s[0] && s[1]) { |
||||
unsigned value; |
||||
if (sscanf(s, "%2x", &value) != 1) break; |
||||
out[n++] = (uint8_t)value; |
||||
s += 2; |
||||
} |
||||
return n; |
||||
} |
||||
|
||||
static int vectors(void) { |
||||
struct reality_traffic send = {0}, recv = {0}; |
||||
uint8_t secret[32], wire[REALITY_RECORD_MAX], plain[REALITY_INNER_MAX], expected[64], type; |
||||
for (unsigned i = 0; i < 32; i++) secret[i] = i; |
||||
CHECK(!reality_traffic_init(&send, secret)); |
||||
CHECK(unhex("1519da96243cb6965fbfa3b28ae25a27", expected) == 16 && !memcmp(send.key, expected, 16)); |
||||
CHECK(unhex("16e5473250aa12b61fbacec7", expected) == 12 && !memcmp(send.iv, expected, 12)); |
||||
const char *golden[] = { |
||||
"1703030018a128808be00c4bdf042cf5ae940649c8f0d4701075afa09b", |
||||
"170303001811fa393d1f1db1a3ee840870bedcc6f5c9387a00c9b41c84" |
||||
}; |
||||
const uint8_t data[] = {1, 2, 3, 0}; |
||||
size_t n, decoded; |
||||
for (unsigned i = 0; i < 2; i++) { |
||||
CHECK(!reality_traffic_init(&send, secret)); |
||||
recv = send; |
||||
send.sequence = recv.sequence = i ? 7 : 0; |
||||
CHECK(!reality_record_seal(&send, 23, data, sizeof(data), 3, wire, sizeof(wire), &n)); |
||||
CHECK(unhex(golden[i], expected) == n && !memcmp(wire, expected, n)); |
||||
CHECK(!reality_record_open(&recv, wire, n, plain, sizeof(plain), &decoded, &type)); |
||||
CHECK(decoded == sizeof(data) && type == 23 && !memcmp(plain, data, decoded)); |
||||
CHECK(reality_record_open(&recv, wire, n, plain, sizeof(plain), &decoded, &type) < 0); // повтор sequence
|
||||
recv.sequence--; |
||||
wire[n - 1] ^= 1; |
||||
CHECK(reality_record_open(&recv, wire, n, plain, sizeof(plain), &decoded, &type) < 0); |
||||
CHECK(recv.sequence == (i ? 7 : 0)); |
||||
} |
||||
CHECK(!reality_traffic_init(&send, secret)); recv = send; |
||||
memset(plain, 0xaa, sizeof(plain)); |
||||
CHECK(!reality_record_seal(&send, 23, plain, 16384, 0, wire, sizeof(wire), &n)); |
||||
CHECK(n == REALITY_RECORD_MAX); |
||||
CHECK(!reality_record_open(&recv, wire, n, plain, sizeof(plain), &decoded, &type) && decoded == 16384); |
||||
CHECK(reality_record_seal(&send, 23, plain, 16384, 1, wire, sizeof(wire), &n) < 0); |
||||
CHECK(reality_record_seal(&send, 22, NULL, 0, 0, wire, sizeof(wire), &n) < 0); |
||||
CHECK(!reality_record_seal(&send, 23, NULL, 0, 0, wire, sizeof(wire), &n)); |
||||
CHECK(!reality_record_open(&recv, wire, n, plain, sizeof(plain), &decoded, &type) && decoded == 0); |
||||
send.sequence = REALITY_KEY_LIMIT; |
||||
CHECK(reality_record_seal(&send, 23, data, sizeof(data), 0, wire, sizeof(wire), &n) < 0); |
||||
uint8_t old_key[16]; memcpy(old_key, send.key, 16); |
||||
CHECK(!reality_traffic_update(&send) && send.sequence == 0 && send.generation == 1 && memcmp(old_key, send.key, 16)); |
||||
recv.sequence = REALITY_KEY_LIMIT; |
||||
CHECK(!reality_traffic_update(&recv)); |
||||
CHECK(!reality_record_seal(&send, 23, data, sizeof(data), 0, wire, sizeof(wire), &n)); |
||||
CHECK(!reality_record_open(&recv, wire, n, plain, sizeof(plain), &decoded, &type)); |
||||
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "HKDF, GCM nonce/AAD, replay, limits, empty DATA and KeyUpdate vectors passed"); |
||||
return 0; |
||||
} |
||||
|
||||
static int hello_session(void) { |
||||
struct reality_session client = {0}, server = {0}; |
||||
struct reality_client_config cc; |
||||
struct reality_server_config sc = {0}; |
||||
reality_client_config_set_defaults(&cc); |
||||
CHECK(!reality_generate_keypair(sc.static_privkey, cc.server_static_pubkey)); |
||||
strcpy(cc.server_name, "www.microsoft.com"); |
||||
sc.version[0] = 2; sc.short_id_count = 1; sc.time_window_sec = 30; |
||||
uint8_t ch[REALITY_MAX_CH_SIZE], sh[REALITY_MAX_SH_SIZE], finished[32], check[32]; |
||||
size_t cn, sn; |
||||
CHECK(!reality_session_client_start(&client, &cc, 1000, ch, sizeof(ch), &cn)); |
||||
CHECK(!reality_session_server_start(&server, &sc, 1000, ch, cn, sh, sizeof(sh), &sn)); |
||||
const size_t offsets[] = {0, 4, 5, 8, 10, 43, 44, 76, 77, 78, 80, 84, 86, 88, 92, 94}; |
||||
for (unsigned i = 0; i < sizeof(offsets) / sizeof(offsets[0]); i++) { |
||||
sh[offsets[i]] ^= 1; |
||||
CHECK(reality_session_client_accept(&client, sh, sn) != REALITY_OK); |
||||
sh[offsets[i]] ^= 1; |
||||
} |
||||
CHECK(!reality_session_client_accept(&client, sh, sn)); |
||||
CHECK(!memcmp(client.send.key, server.recv.key, 16) && !memcmp(client.recv.key, server.send.key, 16)); |
||||
CHECK(memcmp(client.send.key, client.recv.key, 16)); |
||||
CHECK(!reality_finished(&server, 1, finished) && !reality_finished(&client, 0, check) && !memcmp(finished, check, 32)); |
||||
const uint8_t ee[] = {8, 0, 0, 2, 0, 0}; |
||||
CHECK(!reality_transcript_add(&client, ee, sizeof(ee))); |
||||
CHECK(!reality_finished(&client, 0, check) && memcmp(finished, check, 32)); |
||||
CHECK(!reality_transcript_add(&server, ee, sizeof(ee))); |
||||
CHECK(!reality_finished(&server, 1, finished) && !reality_finished(&client, 0, check) && !memcmp(finished, check, 32)); |
||||
for (unsigned i = 0; i < 32; i++) CHECK(!client.auth_key[i] && !server.auth_key[i] && !client.ephemeral_private[i]); |
||||
reality_session_cleanup(&client); reality_session_cleanup(&server); |
||||
|
||||
/* Все открытые байты совпадают с локальным эталоном OpenSSL; динамические поля исключены. */ |
||||
FILE *f = fopen("fixtures/reality_openssl_hello.hex", "r"); |
||||
if (!f) f = fopen("tests/fixtures/reality_openssl_hello.hex", "r"); |
||||
CHECK(f); |
||||
char line[2048]; uint8_t reference[1024]; size_t rn = 0; |
||||
while (fgets(line, sizeof(line), f)) if (line[0] != '#') { rn = unhex(line, reference); break; } |
||||
fclose(f); |
||||
CHECK(rn == cn && cn == 263); |
||||
memset(ch + 11, 0, 32); memset(reference + 11, 0, 32); |
||||
memset(ch + 44, 0, 32); memset(reference + 44, 0, 32); |
||||
memset(ch + cn - 41, 0, 32); memset(reference + cn - 41, 0, 32); |
||||
CHECK(!memcmp(ch, reference, cn)); |
||||
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "Hello validation, independent traffic keys, transcript Finished and OpenSSL profile passed"); |
||||
return 0; |
||||
} |
||||
|
||||
int main(void) { |
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); |
||||
return vectors() || hello_session(); |
||||
} |
||||
@ -0,0 +1,147 @@
|
||||
/* Обычный TLS 1.3 и HTTP-запрос проходят через REALITY fallback на локальный HTTPS. */ |
||||
#define OPENSSL_API_COMPAT 0x10100000L |
||||
#include "stcp_server.h" |
||||
#include "utun_instance.h" |
||||
#include "../lib/debug_config.h" |
||||
#include "../lib/mem.h" |
||||
#include <openssl/ssl.h> |
||||
#include <openssl/x509.h> |
||||
#include <openssl/err.h> |
||||
#include <string.h> |
||||
#include <time.h> |
||||
|
||||
#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "FAIL line=%d: %s", __LINE__, #x); return 1; } } while (0) |
||||
struct https_peer { |
||||
struct UASYNC *ua; |
||||
SSL *ssl; |
||||
void *socket_id; |
||||
socket_t sock; |
||||
uint8_t ready, done, failed; |
||||
uint8_t response[256]; size_t response_len; |
||||
}; |
||||
struct https_server { struct UASYNC *ua; SSL_CTX *ctx; socket_t sock; struct https_peer peer; }; |
||||
static const char response[] = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: close\r\n\r\nhello"; |
||||
|
||||
static void https_progress(socket_t sock, void *arg) { |
||||
struct https_peer *p = arg; |
||||
int rc; |
||||
if (!p->ready) { |
||||
rc = SSL_accept(p->ssl); |
||||
if (rc == 1) p->ready = 1; |
||||
else { |
||||
int error = SSL_get_error(p->ssl, rc); |
||||
if (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE) { |
||||
uasync_set_socket_write(p->ua, p->socket_id, error == SSL_ERROR_WANT_WRITE); |
||||
return; |
||||
} |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "HTTPS handshake failed: ssl=%d", error); |
||||
p->failed = 1; |
||||
return; |
||||
} |
||||
} |
||||
if (!p->done) { |
||||
uint8_t request[512]; |
||||
rc = SSL_read(p->ssl, request, sizeof(request)); |
||||
if (rc > 0) { |
||||
if (rc < 4 || memcmp(request, "GET ", 4)) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "invalid HTTP request"); p->failed = 1; return; } |
||||
rc = SSL_write(p->ssl, response, sizeof(response) - 1); |
||||
if (rc != sizeof(response) - 1) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "HTTPS response write failed"); p->failed = 1; return; } |
||||
p->done = 1; |
||||
uasync_set_socket_write(p->ua, p->socket_id, 0); |
||||
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "local HTTPS received HTTP request through relay"); |
||||
} else { |
||||
int error = SSL_get_error(p->ssl, rc); |
||||
if (error != SSL_ERROR_WANT_READ && error != SSL_ERROR_WANT_WRITE) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "HTTPS read failed: ssl=%d openssl=%lu", error, ERR_get_error()); |
||||
p->failed = 1; |
||||
} |
||||
uasync_set_socket_write(p->ua, p->socket_id, error == SSL_ERROR_WANT_WRITE); |
||||
} |
||||
} |
||||
(void)sock; |
||||
} |
||||
static void https_accept(socket_t sock, void *arg) { |
||||
struct https_server *s = arg; |
||||
struct https_peer *p = &s->peer; |
||||
p->sock = accept(sock, NULL, NULL); p->ua = s->ua; |
||||
if (p->sock == SOCKET_INVALID) { p->failed = 1; return; } |
||||
socket_set_nonblocking(p->sock); |
||||
p->ssl = SSL_new(s->ctx); |
||||
if (!p->ssl || SSL_set_fd(p->ssl, p->sock) != 1) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "HTTPS SSL setup failed"); p->failed = 1; return; } |
||||
p->socket_id = uasync_add_socket_t(s->ua, p->sock, https_progress, https_progress, NULL, "test_https", p); |
||||
if (!p->socket_id) { DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "HTTPS socket registration failed"); p->failed = 1; } |
||||
} |
||||
static void unexpected_stcp(struct stcp_conn *c, void *arg) { |
||||
(void)c; (void)arg; |
||||
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "ordinary HTTPS client entered STCP"); |
||||
} |
||||
static int client_step(SSL *client, int rc) { |
||||
if (rc > 0) return rc; |
||||
int error = SSL_get_error(client, rc); |
||||
return error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE ? 0 : -1; |
||||
} |
||||
|
||||
int main(void) { |
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); |
||||
size_t allocated = u_get_allocated_count(); |
||||
struct UASYNC *ua = uasync_create(); CHECK(ua); |
||||
struct UTUN_INSTANCE inst = {0}; inst.ua = ua; |
||||
struct SC_MYKEYS keys; CHECK(sc_generate_keypair(&keys) == SC_OK); |
||||
EVP_PKEY_CTX *key_ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); EVP_PKEY *key = NULL; |
||||
CHECK(key_ctx && EVP_PKEY_keygen_init(key_ctx) == 1 && EVP_PKEY_keygen(key_ctx, &key) == 1); |
||||
X509 *cert = X509_new(); CHECK(cert); |
||||
CHECK(X509_set_version(cert, 2) && ASN1_INTEGER_set(X509_get_serialNumber(cert), 1)); |
||||
CHECK(X509_gmtime_adj(X509_getm_notBefore(cert), 0) && X509_gmtime_adj(X509_getm_notAfter(cert), 3600)); |
||||
X509_NAME *name = X509_get_subject_name(cert); |
||||
CHECK(X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (const unsigned char *)"localhost", -1, -1, 0)); |
||||
CHECK(X509_set_issuer_name(cert, name) && X509_set_pubkey(cert, key) && X509_sign(cert, key, NULL) > 0); |
||||
struct https_server server = {0}; server.ua = ua; server.peer.sock = SOCKET_INVALID; |
||||
server.ctx = SSL_CTX_new(TLS_server_method()); SSL_CTX *client_ctx = SSL_CTX_new(TLS_client_method()); |
||||
CHECK(server.ctx && client_ctx && SSL_CTX_use_certificate(server.ctx, cert) && SSL_CTX_use_PrivateKey(server.ctx, key)); |
||||
CHECK(SSL_CTX_set_min_proto_version(server.ctx, TLS1_3_VERSION) && SSL_CTX_set_min_proto_version(client_ctx, TLS1_3_VERSION)); |
||||
CHECK(SSL_CTX_set1_groups_list(client_ctx, "X25519") && SSL_CTX_set_ciphersuites(client_ctx, "TLS_AES_128_GCM_SHA256")); |
||||
SSL_CTX_set_num_tickets(server.ctx, 0); SSL_CTX_set_verify(client_ctx, SSL_VERIFY_NONE, NULL); |
||||
server.sock = socket(AF_INET, SOCK_STREAM, 0); CHECK(server.sock != SOCKET_INVALID); |
||||
struct sockaddr_in address = {0}; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
||||
CHECK(!bind(server.sock, (struct sockaddr *)&address, sizeof(address)) && !listen(server.sock, 1)); |
||||
socklen_t size = sizeof(address); CHECK(!getsockname(server.sock, (struct sockaddr *)&address, &size)); |
||||
socket_set_nonblocking(server.sock); |
||||
CHECK(uasync_add_socket_t(ua, server.sock, https_accept, NULL, NULL, "test_https_listener", &server)); |
||||
struct reality_config cfg; reality_config_set_defaults(&cfg); |
||||
cfg.enabled = 1; cfg.short_id_count = 1; |
||||
uint8_t pub[32]; CHECK(!reality_generate_keypair(cfg.private_key, pub)); |
||||
snprintf(cfg.dest, sizeof(cfg.dest), "127.0.0.1:%u", ntohs(address.sin_port)); |
||||
uint16_t port = 34000 + (uint16_t)(time(NULL) % 1000); |
||||
struct stcp_server *reality = stcp_server_create(ua, port, &keys, NULL, &inst, unexpected_stcp, NULL, NULL, NULL, AF_INET); |
||||
CHECK(reality); stcp_server_set_reality(reality, &cfg); |
||||
socket_t sock = socket(AF_INET, SOCK_STREAM, 0); CHECK(sock != SOCKET_INVALID); |
||||
address.sin_port = htons(port); CHECK(!connect(sock, (struct sockaddr *)&address, sizeof(address))); |
||||
socket_set_nonblocking(sock); |
||||
SSL *client = SSL_new(client_ctx); CHECK(client && SSL_set_fd(client, sock)); |
||||
CHECK(SSL_set_tlsext_host_name(client, "localhost")); |
||||
int ready = 0; |
||||
for (unsigned i = 0; !ready && !server.peer.failed && i < 1000; i++) { |
||||
ready = client_step(client, SSL_connect(client)); CHECK(ready >= 0); uasync_poll(ua, 1); |
||||
} |
||||
CHECK(ready == 1 && SSL_version(client) == TLS1_3_VERSION); |
||||
const char request[] = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; |
||||
CHECK(SSL_write(client, request, sizeof(request) - 1) == sizeof(request) - 1); |
||||
uint8_t data[256]; size_t received = 0; |
||||
for (unsigned i = 0; received < sizeof(response) - 1 && !server.peer.failed && i < 1000; i++) { |
||||
int n = client_step(client, SSL_read(client, data + received, sizeof(data) - received)); |
||||
CHECK(n >= 0); received += n; uasync_poll(ua, 1); |
||||
} |
||||
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "HTTPS result: ready=%u done=%u failed=%u received=%zu", server.peer.ready, server.peer.done, server.peer.failed, received); |
||||
CHECK(!server.peer.failed && received == sizeof(response) - 1 && !memcmp(data, response, received)); |
||||
CHECK(inst.reality_owner.relay_count == 1); |
||||
SSL_free(client); socket_close_wrapper(sock); |
||||
stcp_server_destroy(reality); reality_owner_cleanup(&inst.reality_owner); |
||||
if (server.peer.sock != SOCKET_INVALID) { |
||||
uasync_remove_socket_t(ua, server.peer.sock); SSL_free(server.peer.ssl); socket_close_wrapper(server.peer.sock); |
||||
} |
||||
uasync_remove_socket_t(ua, server.sock); socket_close_wrapper(server.sock); |
||||
SSL_CTX_free(client_ctx); SSL_CTX_free(server.ctx); X509_free(cert); EVP_PKEY_free(key); EVP_PKEY_CTX_free(key_ctx); |
||||
uasync_drain_immediate(ua); uasync_destroy(ua, 1); CHECK(u_get_allocated_count() == allocated); |
||||
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "real TLS 1.3 + HTTP over fallback passed without leaked resources"); |
||||
return 0; |
||||
} |
||||
Loading…
Reference in new issue