obfs3 (The Threebfuscator) 0. Protocol overview This is a protocol obfuscation layer for TCP protocols. Its purpose is to keep a third party from telling what protocol is in use based on message contents. Like obfs2, it does not provide authentication or data integrity. It does not hide data lengths. It is more suitable for providing a layer of obfuscation for an existing authenticated protocol, like SSH or TLS. Like obfs2, the protocol has two phases: in the first phase, the parties establish keys. In the second, the parties exchange superenciphered traffic. 1. Motivation The first widely used obfuscation protocol for Tor was obfs2. obfs2 encrypted traffic using a key that was negotiated during the protocol. obfs2 did not use a robust cryptographic key exchange, and the key could be retrieved by any passive adversary who monitored the initial handshake of obfs2. People believe that the easiest way to block obfs2 would be to retrieve the key, decrypt the first bytes of the handshake, and look for redundancy on the handshake message. To defend against this attack, obfs3 negotiates keys using an anonymous Diffie Hellman key exchange. This is done so that a passive adversary would not be able to retrieve the obfs3 session key. Unfortunately, traditional DH (over subgroups of Z_p* or over Elliptic Curves) does not fit our threat model since its public keys are distinguishable from random strings of the same size. For this reason, a custom DH protocol was proposed that offers public keys that look like random strings. The UniformDH scheme was proposed by Ian Goldberg in: https://lists.torproject.org/pipermail/tor-dev/2012-December/004245.html 2. Primitives, notation, and constants. E(K,s) is the AES-CTR-128 encryption of s using K as key. x | y is the concatenation of x and y. WR(n) is n bytes of weaker random data. "xyz" is the ASCII characters 'x', 'y', and 'z', not NULL-terminated. s[:n] is the first n bytes of s. s[n:] is the last n bytes of s. MAX_PADDING is 8194 KEYLEN is the length of the key used by E(K,s) -- that is, 16. COUNTERLEN is the length of the counter used by AES-CTR-128 -- that is, 16. HMAC(k,m) is HMAC-SHA256(k,m) with 'k' being the key, and 'm' the message. A "byte" is an 8-bit octet. 3. UniformDH The UniformDH Diffie-Hellman scheme uses group 5 from RFC3526. It's a 1536-bit MODP group. To pick a private UniformDH key, we pick a random 1536-bit number, and make it even by setting its low bit to 0. Let x be that private key, and X = g^x (mod p). The other party computes private and public keys, y and Y, in the same manner. When someone sends her public key to the other party, she randomly decides whether to send X or p-X. This makes the public key negligibly different from a uniform 1536-bit string When a party wants to calculate the shared secret, she raises the foreign public key to her private key. Note that both (p-Y)^x = Y^x (mod p) and (p-X)^y = X^y (mod p), since x and y are even. 3. Key establishment phase. The party who opens the connection is the 'initiator'; the one who accepts it is the 'responder'. Each begins by generating a UniformDH keypair, and a random number PADLEN in [0, MAX_PADDING/2]. Both parties then send: PUB_KEY | WR(PADLEN) After retrieving the public key of the other end, each party completes the DH key exchange and generates a shared-secret for the session (named SHARED_SECRET). Using that shared-secret each party derives its encryption keys as follows: INIT_SECRET = HMAC(SHARED_SECRET, "Initiator obfuscated data") RESP_SECRET = HMAC(SHARED_SECRET, "Responder obfuscated data") INIT_KEY = INIT_SECRET[:KEYLEN] INIT_COUNTER = INIT_SECRET[KEYLEN:] RESP_KEY = RESP_SECRET[:KEYLEN] RESP_COUNTER = RESP_SECRET[KEYLEN:] The INIT_KEY value keys a block cipher (in CTR mode) used to encrypt values from initiator to responder thereafter. The counter mode's initial counter value is INIT_COUNTER. The RESP_KEY value keys a block cipher (in CTR mode) used to encrypt values from responder to initiator thereafter. That counter mode's initial counter value is RESP_COUNTER. After the handshake is complete, when the initiator wants to send application-layer data for the first time, she generates another random number PADLEN2 in [0, MAX_PADDING/2], and sends: WR(PADLEN2) | HMAC(SHARED_SECRET, "Initiator magic") | E(INIT_KEY, DATA) When the responder wants to send application-layer data for the first time, she sends: WR(PADLEN2) | HMAC(SHARED_SECRET, "Responder magic") | E(RESP_KEY, DATA) After a party receives the public key from the other end, it needs to find out where the padding stops and where the application-layer data starts. To do so, every time she receives network data, the receiver tries to find the magic HMAC string in the data between the public key and the end of the newly received data. After spotting the magic string, she knows where the application-layer data starts and she can start decrypting it. If a party has scanned more than MAX_PADDING bytes and the magic string has not yet been found, the party MUST close the connection. After the initiator sends the magic string and the first chunk of application-layer data, she can send additional application-layer data simply by encrypting it with her encryption key, and without prepending any magic strings: E(INIT_KEY, DATA) Similarly, the responder sends additional application-layer data by encrypting it with her encryption key: E(RESP_KEY, DATA) 4. Acknowledgments The idea of using a hash of the shared secret as the delimiter between the padding and the data was suggested by Philipp Winter. Ian Goldberg suggested the UniformDH scheme and helped a lot with reviewing the protocol specification.