Skip to content

Commit 00eef49

Browse files
Dendi Suhubdyclaude
andcommitted
Fix obfs4 AUTH mismatch + framing: ntor string lengths, Y value, SipHash DRBG
Critical fixes for obfs4 interoperability with Go obfs4proxy/lyrebird: - Fix T_KEY_LEN (37→36), T_VERIFY_LEN (36→35), M_EXPAND_LEN (36→35): all included null terminator byte, corrupting every HMAC/HKDF operation - Fix Y in secret_input: use Elligator2 scalar_base_mult public key (what client recovers from representative), not OpenSSL's public key - Implement SipHash-2-4 DRBG for frame length obfuscation per obfs4 spec - Fix framing: 2-byte XOR length + secretbox(payload), one nonce/frame - Extract DRBG seed (bytes 48-72) from each 72-byte HKDF key block Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f7f278d commit 00eef49

File tree

3 files changed

+186
-52
lines changed

3 files changed

+186
-52
lines changed

include/tor/transport/obfs4.hpp

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ constexpr size_t OBFS4_AUTH_LEN = 32; // Server authentication (ntor AU
2525
constexpr size_t OBFS4_CERT_RAW_LEN = 52; // node_id[20] + pubkey[32]
2626
constexpr size_t OBFS4_MAX_HANDSHAKE_LEN = 8192; // Max bytes to buffer for handshake
2727
constexpr size_t OBFS4_MAX_FRAME_PAYLOAD = 1448; // Max frame payload (MTU-friendly)
28-
constexpr size_t OBFS4_FRAME_HDR_OVERHEAD = 2 + crypto::Secretbox::OVERHEAD; // 18 bytes
29-
constexpr size_t OBFS4_FRAME_OVERHEAD = OBFS4_FRAME_HDR_OVERHEAD;
28+
constexpr size_t OBFS4_FRAME_HDR_LEN = 2; // Obfuscated length (DRBG XOR)
29+
constexpr size_t OBFS4_FRAME_OVERHEAD = OBFS4_FRAME_HDR_LEN + crypto::Secretbox::OVERHEAD; // 2 + 16 = 18
3030
constexpr size_t OBFS4_MIN_CLIENT_HANDSHAKE = OBFS4_REPR_LEN + OBFS4_MARK_LEN + OBFS4_MAC_LEN; // 64
3131
constexpr size_t OBFS4_MIN_SERVER_HANDSHAKE = OBFS4_REPR_LEN + OBFS4_AUTH_LEN + OBFS4_MARK_LEN + OBFS4_MAC_LEN; // 96
3232

@@ -97,11 +97,14 @@ class Obfs4ServerHandshake {
9797
[[nodiscard]] State state() const { return state_; }
9898

9999
// Session keys (available after Completed state)
100+
// Each direction: secretbox_key[32] | nonce_prefix[16] | drbg_seed[24]
100101
struct SessionKeys {
101-
std::array<uint8_t, 32> send_key; // Server -> Client
102-
std::array<uint8_t, 32> recv_key; // Client -> Server
103-
std::array<uint8_t, 24> send_nonce; // Initial send nonce
104-
std::array<uint8_t, 24> recv_nonce; // Initial recv nonce
102+
std::array<uint8_t, 32> send_key; // Server -> Client secretbox key
103+
std::array<uint8_t, 32> recv_key; // Client -> Server secretbox key
104+
std::array<uint8_t, 24> send_nonce; // Initial send nonce (prefix[16] || counter[8])
105+
std::array<uint8_t, 24> recv_nonce; // Initial recv nonce (prefix[16] || counter[8])
106+
std::array<uint8_t, 24> send_drbg_seed; // SipHash DRBG seed for send length obfuscation
107+
std::array<uint8_t, 24> recv_drbg_seed; // SipHash DRBG seed for recv length deobfuscation
105108
};
106109

107110
[[nodiscard]] const SessionKeys& session_keys() const { return session_keys_; }
@@ -145,20 +148,47 @@ class Obfs4ServerHandshake {
145148
const crypto::Curve25519PublicKey& server_eph_pub);
146149
};
147150

151+
// --- SipHash-2-4 DRBG ---
152+
153+
// Deterministic random bit generator using SipHash-2-4 in OFB mode.
154+
// Used for obfs4 frame length obfuscation per the obfs4 spec.
155+
class Obfs4Drbg {
156+
public:
157+
Obfs4Drbg() = default;
158+
159+
// Initialize from 24-byte seed: siphash_key[16] || initial_ofb[8]
160+
void init(std::span<const uint8_t, 24> seed);
161+
162+
// Generate next 8-byte block of DRBG output
163+
[[nodiscard]] std::array<uint8_t, 8> next_block();
164+
165+
// Generate a 2-byte length mask for frame length obfuscation
166+
[[nodiscard]] uint16_t next_length_mask();
167+
168+
private:
169+
std::array<uint8_t, 16> key_{};
170+
std::array<uint8_t, 8> ofb_{};
171+
bool initialized_{false};
172+
};
173+
148174
// --- obfs4 Framing ---
149175

150-
// Encrypt/decrypt obfs4 frames using NaCl secretbox with incrementing nonces
176+
// Encrypt/decrypt obfs4 frames per the obfs4 spec:
177+
// Frame = obfuscated_length[2] || secretbox_seal(payload)
178+
// Length is XOR'd with SipHash-2-4 DRBG output for obfuscation.
151179
class Obfs4Framing {
152180
public:
153181
Obfs4Framing() = default;
154182

155-
// Initialize with session keys
183+
// Initialize with session keys and DRBG seeds
156184
void init_send(std::span<const uint8_t, 32> key,
157-
std::span<const uint8_t, 24> initial_nonce);
185+
std::span<const uint8_t, 24> initial_nonce,
186+
std::span<const uint8_t, 24> drbg_seed);
158187
void init_recv(std::span<const uint8_t, 32> key,
159-
std::span<const uint8_t, 24> initial_nonce);
188+
std::span<const uint8_t, 24> initial_nonce,
189+
std::span<const uint8_t, 24> drbg_seed);
160190

161-
// Encode a frame: returns secretbox_seal(len[2]) || secretbox_seal(payload)
191+
// Encode a frame: returns obfuscated_length[2] || secretbox_seal(payload)
162192
[[nodiscard]] std::vector<uint8_t> encode(std::span<const uint8_t> payload);
163193

164194
// Decode frames from incoming data.
@@ -175,18 +205,20 @@ class Obfs4Framing {
175205
// Send state
176206
std::array<uint8_t, 32> send_key_{};
177207
std::array<uint8_t, 24> send_nonce_{};
208+
Obfs4Drbg send_drbg_;
178209
bool send_initialized_{false};
179210

180211
// Receive state
181212
std::array<uint8_t, 32> recv_key_{};
182213
std::array<uint8_t, 24> recv_nonce_{};
214+
Obfs4Drbg recv_drbg_;
183215
bool recv_initialized_{false};
184216

185217
// Receive buffer for partial frames
186218
std::vector<uint8_t> recv_buffer_;
187219
std::optional<uint16_t> pending_payload_len_;
188220

189-
// Increment nonce (little-endian counter)
221+
// Increment nonce (big-endian counter in last 8 bytes)
190222
static void increment_nonce(std::array<uint8_t, 24>& nonce);
191223
};
192224

src/transport/obfs4.cpp

Lines changed: 139 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ static constexpr const char T_MAC[] = "ntor-curve25519-sha256-1:mac";
1515
static constexpr size_t T_MAC_LEN = 28;
1616

1717
static constexpr const char T_KEY[] = "ntor-curve25519-sha256-1:key_extract";
18-
static constexpr size_t T_KEY_LEN = 37;
18+
static constexpr size_t T_KEY_LEN = 36; // strlen, NOT sizeof (no null byte)
1919

2020
static constexpr const char T_VERIFY[] = "ntor-curve25519-sha256-1:key_verify";
21-
static constexpr size_t T_VERIFY_LEN = 36;
21+
static constexpr size_t T_VERIFY_LEN = 35; // strlen, NOT sizeof (no null byte)
2222

2323
static constexpr const char M_EXPAND[] = "ntor-curve25519-sha256-1:key_expand";
24-
static constexpr size_t M_EXPAND_LEN = 36;
24+
static constexpr size_t M_EXPAND_LEN = 35; // strlen, NOT sizeof (no null byte)
2525

2626
// --- Utility ---
2727

@@ -235,8 +235,12 @@ Obfs4ServerHandshake::consume(std::span<const uint8_t> data) {
235235
return std::unexpected(Obfs4Error::HandshakeFailed);
236236
}
237237

238-
// Server ephemeral public key (Y)
239-
auto server_eph_pub = eph_sk->public_key();
238+
// Server ephemeral public key (Y) — must use the Elligator2-derived
239+
// public key (from scalar_base_mult), NOT OpenSSL's public key.
240+
// The client recovers Y from our representative via representative_to_point(),
241+
// which returns the scalar_base_mult result. Using OpenSSL's public key here
242+
// would cause an AUTH mismatch if the two derivations differ.
243+
auto server_eph_pub = crypto::Curve25519PublicKey(server_ephemeral_->public_key);
240244

241245
// Derive ntor keys: KEY_SEED, verify, auth, session keys
242246
derive_keys(*exp_eph, *exp_id,
@@ -467,24 +471,26 @@ void Obfs4ServerHandshake::derive_keys(
467471
// okm[72:144] = client decoder key / server encoder key
468472
//
469473
// Each 72-byte block: secretbox_key[32] | nonce_prefix[16] | drbg_seed[24]
470-
// For now, map into current SessionKeys format (32-byte key + 24-byte nonce)
471474

472475
// Server recv (client encoder) = okm[0:72]
473476
std::memcpy(session_keys_.recv_key.data(), km.data(), 32);
474477
// Build recv nonce: prefix[16] || counter[8] with counter=1
475478
std::array<uint8_t, 24> recv_nonce{};
476479
std::memcpy(recv_nonce.data(), km.data() + 32, 16);
477-
// Counter starts at 1 (big-endian)
478-
recv_nonce[23] = 1;
480+
recv_nonce[23] = 1; // Counter starts at 1 (big-endian)
479481
std::memcpy(session_keys_.recv_nonce.data(), recv_nonce.data(), 24);
482+
// DRBG seed for recv direction: okm[48:72]
483+
std::memcpy(session_keys_.recv_drbg_seed.data(), km.data() + 48, 24);
480484

481485
// Server send (client decoder) = okm[72:144]
482486
std::memcpy(session_keys_.send_key.data(), km.data() + 72, 32);
483487
// Build send nonce: prefix[16] || counter[8] with counter=1
484488
std::array<uint8_t, 24> send_nonce{};
485489
std::memcpy(send_nonce.data(), km.data() + 72 + 32, 16);
486-
send_nonce[23] = 1;
490+
send_nonce[23] = 1; // Counter starts at 1 (big-endian)
487491
std::memcpy(session_keys_.send_nonce.data(), send_nonce.data(), 24);
492+
// DRBG seed for send direction: okm[120:144]
493+
std::memcpy(session_keys_.send_drbg_seed.data(), km.data() + 72 + 48, 24);
488494

489495
// Wipe sensitive data
490496
std::memset(secret_input.data(), 0, secret_input.size());
@@ -544,19 +550,118 @@ Obfs4ServerHandshake::generate_server_hello() {
544550
return hello;
545551
}
546552

553+
// --- SipHash-2-4 ---
554+
555+
namespace {
556+
557+
inline uint64_t rotl64(uint64_t v, int n) {
558+
return (v << n) | (v >> (64 - n));
559+
}
560+
561+
inline void sipround(uint64_t& v0, uint64_t& v1, uint64_t& v2, uint64_t& v3) {
562+
v0 += v1; v1 = rotl64(v1, 13); v1 ^= v0; v0 = rotl64(v0, 32);
563+
v2 += v3; v3 = rotl64(v3, 16); v3 ^= v2;
564+
v0 += v3; v3 = rotl64(v3, 21); v3 ^= v0;
565+
v2 += v1; v1 = rotl64(v1, 17); v1 ^= v2; v2 = rotl64(v2, 32);
566+
}
567+
568+
inline uint64_t le64(const uint8_t* p) {
569+
return static_cast<uint64_t>(p[0])
570+
| (static_cast<uint64_t>(p[1]) << 8)
571+
| (static_cast<uint64_t>(p[2]) << 16)
572+
| (static_cast<uint64_t>(p[3]) << 24)
573+
| (static_cast<uint64_t>(p[4]) << 32)
574+
| (static_cast<uint64_t>(p[5]) << 40)
575+
| (static_cast<uint64_t>(p[6]) << 48)
576+
| (static_cast<uint64_t>(p[7]) << 56);
577+
}
578+
579+
inline void put_le64(uint8_t* p, uint64_t v) {
580+
p[0] = static_cast<uint8_t>(v);
581+
p[1] = static_cast<uint8_t>(v >> 8);
582+
p[2] = static_cast<uint8_t>(v >> 16);
583+
p[3] = static_cast<uint8_t>(v >> 24);
584+
p[4] = static_cast<uint8_t>(v >> 32);
585+
p[5] = static_cast<uint8_t>(v >> 40);
586+
p[6] = static_cast<uint8_t>(v >> 48);
587+
p[7] = static_cast<uint8_t>(v >> 56);
588+
}
589+
590+
// SipHash-2-4: hash an 8-byte message with a 16-byte key, producing 8 bytes
591+
uint64_t siphash_2_4(const uint8_t key[16], const uint8_t msg[8]) {
592+
uint64_t k0 = le64(key);
593+
uint64_t k1 = le64(key + 8);
594+
595+
uint64_t v0 = k0 ^ 0x736f6d6570736575ULL;
596+
uint64_t v1 = k1 ^ 0x646f72616e646f6dULL;
597+
uint64_t v2 = k0 ^ 0x6c7967656e657261ULL;
598+
uint64_t v3 = k1 ^ 0x7465646279746573ULL;
599+
600+
// Process single 8-byte block
601+
uint64_t m = le64(msg);
602+
v3 ^= m;
603+
sipround(v0, v1, v2, v3);
604+
sipround(v0, v1, v2, v3);
605+
v0 ^= m;
606+
607+
// Finalization: length byte (8) in high byte of last block
608+
uint64_t b = static_cast<uint64_t>(8) << 56;
609+
v3 ^= b;
610+
sipround(v0, v1, v2, v3);
611+
sipround(v0, v1, v2, v3);
612+
v0 ^= b;
613+
614+
v2 ^= 0xff;
615+
sipround(v0, v1, v2, v3);
616+
sipround(v0, v1, v2, v3);
617+
sipround(v0, v1, v2, v3);
618+
sipround(v0, v1, v2, v3);
619+
620+
return v0 ^ v1 ^ v2 ^ v3;
621+
}
622+
623+
} // anonymous namespace
624+
625+
// --- Obfs4Drbg ---
626+
627+
void Obfs4Drbg::init(std::span<const uint8_t, 24> seed) {
628+
std::memcpy(key_.data(), seed.data(), 16);
629+
std::memcpy(ofb_.data(), seed.data() + 16, 8);
630+
initialized_ = true;
631+
}
632+
633+
std::array<uint8_t, 8> Obfs4Drbg::next_block() {
634+
// OFB mode: ofb = SipHash-2-4(key, ofb)
635+
uint64_t output = siphash_2_4(key_.data(), ofb_.data());
636+
put_le64(ofb_.data(), output);
637+
638+
std::array<uint8_t, 8> result;
639+
put_le64(result.data(), output);
640+
return result;
641+
}
642+
643+
uint16_t Obfs4Drbg::next_length_mask() {
644+
auto block = next_block();
645+
return static_cast<uint16_t>(block[0]) << 8 | static_cast<uint16_t>(block[1]);
646+
}
647+
547648
// --- Obfs4Framing ---
548649

549650
void Obfs4Framing::init_send(std::span<const uint8_t, 32> key,
550-
std::span<const uint8_t, 24> initial_nonce) {
651+
std::span<const uint8_t, 24> initial_nonce,
652+
std::span<const uint8_t, 24> drbg_seed) {
551653
std::memcpy(send_key_.data(), key.data(), 32);
552654
std::memcpy(send_nonce_.data(), initial_nonce.data(), 24);
655+
send_drbg_.init(drbg_seed);
553656
send_initialized_ = true;
554657
}
555658

556659
void Obfs4Framing::init_recv(std::span<const uint8_t, 32> key,
557-
std::span<const uint8_t, 24> initial_nonce) {
660+
std::span<const uint8_t, 24> initial_nonce,
661+
std::span<const uint8_t, 24> drbg_seed) {
558662
std::memcpy(recv_key_.data(), key.data(), 32);
559663
std::memcpy(recv_nonce_.data(), initial_nonce.data(), 24);
664+
recv_drbg_.init(drbg_seed);
560665
recv_initialized_ = true;
561666
}
562667

@@ -569,27 +674,27 @@ void Obfs4Framing::increment_nonce(std::array<uint8_t, 24>& nonce) {
569674
}
570675

571676
std::vector<uint8_t> Obfs4Framing::encode(std::span<const uint8_t> payload) {
572-
// Frame format:
573-
// secretbox_seal(length[2]) || secretbox_seal(payload)
574-
// where length is big-endian uint16
575-
// NOTE: Real obfs4 uses SipHash XOR for length, not secretbox.
576-
// This will be fixed in a follow-up commit.
577-
578-
std::vector<uint8_t> output;
677+
// Frame format per obfs4 spec:
678+
// obfuscated_length[2] || secretbox_seal(payload)
679+
//
680+
// obfuscated_length = payload_len XOR drbg.next_length_mask()
681+
// secretbox uses one nonce per frame (incremented after payload seal)
579682

580683
uint16_t len = static_cast<uint16_t>(payload.size());
581-
std::array<uint8_t, 2> len_bytes = {
582-
static_cast<uint8_t>(len >> 8),
583-
static_cast<uint8_t>(len & 0xff)
584-
};
684+
uint16_t mask = send_drbg_.next_length_mask();
685+
uint16_t obfuscated = len ^ mask;
585686

586-
auto sealed_len = crypto::Secretbox::seal(send_key_, send_nonce_, len_bytes);
587-
increment_nonce(send_nonce_);
687+
std::vector<uint8_t> output;
688+
output.reserve(2 + payload.size() + crypto::Secretbox::OVERHEAD);
689+
690+
// 1. Obfuscated length (2 bytes, big-endian)
691+
output.push_back(static_cast<uint8_t>(obfuscated >> 8));
692+
output.push_back(static_cast<uint8_t>(obfuscated & 0xff));
588693

694+
// 2. Secretbox-sealed payload
589695
auto sealed_payload = crypto::Secretbox::seal(send_key_, send_nonce_, payload);
590696
increment_nonce(send_nonce_);
591697

592-
output.insert(output.end(), sealed_len.begin(), sealed_len.end());
593698
output.insert(output.end(), sealed_payload.begin(), sealed_payload.end());
594699

595700
return output;
@@ -604,28 +709,25 @@ Obfs4Framing::decode(std::span<const uint8_t> data) {
604709

605710
while (true) {
606711
if (!pending_payload_len_) {
607-
constexpr size_t SEALED_LEN_SIZE = 2 + crypto::Secretbox::OVERHEAD;
608-
if (recv_buffer_.size() < SEALED_LEN_SIZE) {
712+
// Need 2 bytes for the obfuscated length
713+
if (recv_buffer_.size() < OBFS4_FRAME_HDR_LEN) {
609714
break;
610715
}
611716

612-
auto len_ct = std::span<const uint8_t>(recv_buffer_.data(), SEALED_LEN_SIZE);
613-
auto len_pt = crypto::Secretbox::open(recv_key_, recv_nonce_, len_ct);
614-
if (!len_pt) {
615-
return std::unexpected(Obfs4Error::FrameDecryptFailed);
616-
}
617-
increment_nonce(recv_nonce_);
717+
// Deobfuscate length: XOR with DRBG output
718+
uint16_t obfuscated = (static_cast<uint16_t>(recv_buffer_[0]) << 8) |
719+
static_cast<uint16_t>(recv_buffer_[1]);
720+
uint16_t mask = recv_drbg_.next_length_mask();
721+
uint16_t payload_len = obfuscated ^ mask;
618722

619-
uint16_t payload_len = (static_cast<uint16_t>((*len_pt)[0]) << 8) |
620-
static_cast<uint16_t>((*len_pt)[1]);
621723
if (payload_len > OBFS4_MAX_FRAME_PAYLOAD) {
622724
return std::unexpected(Obfs4Error::FrameTooLarge);
623725
}
624726

625727
pending_payload_len_ = payload_len;
626728
recv_buffer_.erase(recv_buffer_.begin(),
627-
recv_buffer_.begin() + SEALED_LEN_SIZE);
628-
result.consumed += SEALED_LEN_SIZE;
729+
recv_buffer_.begin() + OBFS4_FRAME_HDR_LEN);
730+
result.consumed += OBFS4_FRAME_HDR_LEN;
629731
}
630732

631733
size_t sealed_payload_size = *pending_payload_len_ + crypto::Secretbox::OVERHEAD;

src/transport/obfs4_listener.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,11 @@ void Obfs4Listener::handle_connection(std::shared_ptr<net::TcpConnection> conn)
133133
completed->fetch_add(1, std::memory_order_relaxed);
134134
LOG_INFO("obfs4 handshake completed successfully");
135135

136-
// Set up framing with session keys
136+
// Set up framing with session keys and DRBG seeds
137137
auto framing = std::make_unique<Obfs4Framing>();
138138
const auto& keys = handshake->session_keys();
139-
framing->init_send(keys.send_key, keys.send_nonce);
140-
framing->init_recv(keys.recv_key, keys.recv_nonce);
139+
framing->init_send(keys.send_key, keys.send_nonce, keys.send_drbg_seed);
140+
framing->init_recv(keys.recv_key, keys.recv_nonce, keys.recv_drbg_seed);
141141

142142
// Connect to local OR port and start proxying
143143
auto or_conn = std::make_shared<net::TcpConnection>(*io_ctx);

0 commit comments

Comments
 (0)