I feel a little bad delaying this blog post for so long. A few other things got into the pipeline. The tl;dr is WSJT-X 2.7.0-rc7 is pretty fine. It uses TOTP now and third parties can run their own verification.

In previous posts we looked into the now discontinued SuperFox verification scheme. Since that post rc7 has been released. Many various solutions were proposed with various trade offs. Remember that we are dealing with dozens of bits here, not bytes. The final solution was chosen for its simplicity.

TOTP

One of our first suggestions when digging into alternatives was using a time based one time password (TOTP), similar to the H40WA dxpedition. TOTP works by both parties having a shared secret key. Both parties combine the time and the secret key, then hash the result. This allows verification without sharing the secret key or being able to replay an old message.

At this point you’ve probably spotted a problem however, both parties need the key. The way this is worked around, both with the H40WA system and the WSJT-X system is that the receiving station doesn’t hold the key. Instead a server holds the key and the client can request the final value from the server.

% curl https://www.9dx.cc/check/K1JT.text
2024-11-22T08:49:00Z K1JT 229106
2024-11-22T08:49:30Z K1JT 305435
2024-11-22T08:50:00Z K1JT 806080
2024-11-22T08:50:30Z K1JT 869989
2024-11-22T08:51:00Z K1JT 699026
2024-11-22T08:51:30Z K1JT 250722
2024-11-22T08:52:00Z K1JT 485396
2024-11-22T08:52:30Z K1JT 429621
2024-11-22T08:53:00Z K1JT 450221
2024-11-22T08:53:30Z K1JT 763383
2024-11-22T08:54:00Z K1JT 182034

The important part that lets this work is that the server only return the TOTP values for already transmitted windows. A transmission using an old code won’t have the correct timestamp and the client won’t verify it.

Generally speaking WSJT-X stations have time set pretty well as transmission and reception only really works if clocks are within 1 second. If a client did have their time set in the past an attacker could use old codes, however in practice this is rare and the consequence is minimal.

WSJT-X actually uses the following request to perform checks

% curl https://www.9dx.cc/check/N9ADG/2024-09-05T19:07:00Z/866973.text
2024-09-05T19:07:00Z N9ADG 866973 VERIFIED

The actual code that validates the response is pretty basic and is just checking that it sees VERIFIED at the end

  if (verify_message.endsWith(" VERIFIED")) {
    return QString("%1   0  0.0 %2 ~  %3 verified").arg(rx_time.toString("hhmmss")).arg(hz).arg(callsign);
  } else
    if (verify_message.endsWith(" INVALID"))
    {
      return QString("%1   0  0.0 %2 ~  %3 invalid").arg(rx_time.toString("hhmmss")).arg(hz).arg(callsign);
    }
    else
      return QString{};

Side note here - The way WSJT-X handles data during a lot of the verification steps is passing through strings then checking if they contain certain substrings. I haven’t spotted a trivial way of doing due to the limited nature of an FT8 transmission (no lowercase, short freetext and no new lines) - but I wouldn’t be surprised if there is some fun to be had here. It might be possible to have the client process a crafted line with an old time.

WSJT-X have listened to feedback and allow running of SuperFox without verification keys. Not only that they do have configuration option to change the verification server. An example server might be something like:

from http.server import HTTPServer, BaseHTTPRequestHandler

class MyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(bytes("meow meow VERIFIED", "utf-8"))

httpd = HTTPServer(('localhost', 8999), MyHandler)
httpd.serve_forever()

Do note that you’ll need to be able to handle many amateur radio operators requesting tokens every 0 and 30 seconds.

It’s really good that the developers have listened to feedback. To recap:

I did quickly want to touch on some of the downsides of this approach. While the transmitting station doesn’t require an internet connection the receivers do. The server does allow checking of old logs so temporary internet outages can be patched over. I think this is a fair compromise and likely works for 99.9% of users. For the users it doesn’t work for - it just means waiting for a QSL card to confirm that it wasn’t a pirate.

Some other approaches used CSPRNG (Cryptographically secure pseudorandom number generator) stepped backwards. Occasionally the broadcasting station would transmit a signed message with the parameters for the CSPRNG which would allow verifying past messages. This was a pretty cool approach even if complicated.