@@ -15,13 +15,13 @@ static constexpr const char T_MAC[] = "ntor-curve25519-sha256-1:mac";
1515static constexpr size_t T_MAC_LEN = 28 ;
1616
1717static 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
2020static 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
2323static 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
549650void 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
556659void 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
571676std::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;
0 commit comments