obfs4proxy-0.0.13 is passively distinguishable because it only sends representatives of public keys that are on the prime-order subgroup of Curve25519
obfs4proxy-0.0.13 (the current version) produces Elligator-encoded public key representatives that are distinguishable from random, because they always decode to points that are on the prime-order subgroup of Curve25519. Random strings decode to points on the prime-order subgroup only 1/4 of the time. This is an instance of a pitfall described by Loup Vaillant:
- https://elligator.org/key-exchange, heading "Step 2"
- https://loup-vaillant.fr/articles/implementing-elligator, heading "Dodging a bullet"
- https://www.reddit.com/r/crypto/comments/fd9t3m/elligator_with_x25519_is_broken_what_workaround/
Curve25519 has order 8×q, where q is a large prime. Therefore points on the curve can only have order 1, 2, 4, 8, q, 2q, 4q, or 8q. Points on the prime-order subgroup or size q have order q. You can efficiently check whether a point has order q by multiplying the point by q and checking if the result is the identity. Attached is a script that does this test against a remote obfs4 server:
The script runs multiple trials to improve the detection rate. The detection rate per trial is a little less than 3/4 (I think it's 21/32), because versions of obfs4proxy before and after commit 393aca86cc (i.e., before and after obfs4proxy-0.0.12) have different ways of interpreting public keys from byte strings (which is the cause of tpo/applications/tor-browser#40804 (closed)), and the script conservatively considers a point to be off the prime-order subgroup only if it is off the subgroup according to both interpretations.
Here the detection script is running against one of the Tor Browser default bridges, and showing that it is distinguishable from random:
$ ./obfs4-subgroup-check 192.95.36.142:443 qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ
............... 192.95.36.142:443 FAIL 0/15
In fact, all the default bridges are distinguishable from random:
$ perl -an -e '/^obfs4 (\S*) (?:\S+) cert=(\S+)/ && print "$1 $2\n";' tor-browser-build/projects/common/bridges_list.obfs4.txt | while read addr cert; do ./obfs4-subgroup-check $addr $cert; done
............... 192.95.36.142:443 FAIL 0/15
............... 38.229.1.78:80 FAIL 0/15
............... 38.229.33.83:80 FAIL 0/15
............... 37.218.245.14:38224 FAIL 0/15
............... 85.31.186.98:443 FAIL 0/15
............... 85.31.186.26:443 FAIL 0/15
............... 193.11.166.194:27015 FAIL 0/15
............... 193.11.166.194:27020 FAIL 0/15
............... 193.11.166.194:27025 FAIL 0/15
............... 209.148.46.65:443 FAIL 0/15
............... 146.57.248.225:22 FAIL 0/15
............... 45.145.95.6:27015 FAIL 0/15
............... [2a0c:4d80:42:702::1]:27015 FAIL 0/15
............... 51.222.13.177:80 FAIL 0/15
It looks like this when run on a random stream. The #
denote trials where the encoded representative decoded to a public key not on the prime-order subgroup.
$ ncat 127.0.0.1 31337 -l -k --sh-exec 'cat /dev/urandom'
$ ./obfs4-subgroup-check 127.0.0.1:31337 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#####.####...## 127.0.0.1:31337 PASS 11/15
The script uses secret information about the bridge, namely its cert
parameter, in order to provoke the server into sending a response, but the core check only requires looking at the first 32 bytes of the response, and works even if you don't know the cert
parameter. In other words, this test could be done by a passive observer or by processing pcaps.
How to reproduce locally
Build a obfs4proxy v0.0.13:
$ git clone https://gitlab.com/yawning/obfs4
$ cd obfs4/obfs4proxy
$ git checkout obfs4proxy-0.0.13
$ go build
Create this torrc file:
SocksPort 0
ORPort 127.0.0.1:auto
PublishServerDescriptor 0
BridgeRelay 1
DataDirectory datadir
ServerTransportListenAddr obfs4 127.0.0.1:12345
ServerTransportPlugin obfs4 exec ./obfs4proxy -unsafeLogging -logLevel DEBUG -enableLogging
Run tor:
$ tor -f torrc
Grab the cert
parameter from datadir/pt_state/obfs4_bridgeline.txt. Run the bug checker script and see "FAIL":
$ ./obfs4-subgroup-check 127.0.0.1:12345 hNzHgvwEUmPV7PhuhRasLGyWx/TjnCIUxYcOQ7fDl2p7EmQ9Tm8EzLmuAtevceE6LoS9Dw
............... 127.0.0.1:12345 FAIL 0/15