Minecraft server lists reward players for voting: the list sends a small message to the game
server, and a plugin like VotingPlugin hands out the reward. The protocol behind this is
Votifier, today mostly in its fork NuVotifier. If you run a server, the most common
question is "is my Votifier actually reachable, and is my token right?". So we wrote a tiny
test client, and it turned out to be a nice little exercise in sockets, HMAC and even raw RSA.
All code below runs on Python 3.8+ without any packages.
The v2 handshake
When you open a TCP connection to the Votifier port (default 8192), the server speaks first:
VOTIFIER 2 4f0c1b8e6d... The third token is a challenge. It ties your vote to this one connection, so a captured
vote can't be replayed later.
def read_greeting(sock): data = b"" while not data.endswith(b"\n"): chunk = sock.recv(1) if not chunk: break data += chunk return data.decode().strip() # "VOTIFIER 2 <challenge>" Building the signed message
The vote itself is a JSON object, serialized to a string. That string is signed with
HMAC-SHA256 using the server's token, and both go into an outer JSON object:
payload = json.dumps({ "serviceName": "votifier-test", "username": "Notch", "address": "127.0.0.1", "timestamp": int(time.time() * 1000), "challenge": challenge, }, separators=(",", ":")) signature = base64.b64encode( hmac.new(token.encode(), payload.encode(), hashlib.sha256).digest() ).decode() message = json.dumps({"payload": payload, "signature": signature}).encode() Note that the payload is a JSON string inside JSON. The server verifies the HMAC over the
exact bytes of that string, so you must not re-serialize it on the way.
Framing: a magic number and a length
NuVotifier v2 frames the message with two big-endian 16-bit integers: the magic 0x733A
and the length of the message.
packet = struct.pack(">HH", 0x733A, len(message)) + message sock.sendall(packet) The server answers with one JSON line: {"status":"ok"} or
{"status":"error","cause":"CorruptedFrameException","error":"Signature is not valid (invalid token?)"}.
That error message is exactly what you want a test tool to show, because "wrong token" is the
number one setup mistake.
Protocol v1: RSA without a crypto library
Old Votifier (v1) has no token. Instead, the vote is a plain text block
VOTE <serviceName> <username> <address> <timestamp> encrypted with the server's 2048-bit RSA public key using PKCS#1 v1.5 padding, and sent as
256 raw bytes. No reply.
The key file (rsa/public.key) is a base64 X.509 SubjectPublicKeyInfo. Getting the modulus
and exponent out of it only needs a minimal DER reader:
def der_read(data, pos): tag = data[pos]; pos += 1 length = data[pos]; pos += 1 if length & 0x80: n = length & 0x7F length = int.from_bytes(data[pos:pos + n], "big"); pos += n return tag, data[pos:pos + length], pos + length SubjectPublicKeyInfo is a SEQUENCE of an AlgorithmIdentifier and a BIT STRING; the BIT STRING
(after one "unused bits" byte) contains another SEQUENCE with the modulus and the exponent.
PKCS#1 v1.5 encryption padding is 00 02 <non-zero random bytes> 00 <message>, filled up to
the key size, and the encryption itself is one line thanks to Python's three-argument pow:
k = (n.bit_length() + 7) // 8 padding = bytes(b for b in os.urandom(k * 2) if b != 0)[: k - 3 - len(message)] block = b"\x00\x02" + padding + b"\x00" + message cipher = pow(int.from_bytes(block, "big"), e, n).to_bytes(k, "big") (Don't use hand-rolled RSA for anything that protects secrets. Here it only reproduces what a
server list sends, and the server decrypts it with a proper library.)
Testing it
We tested three things against a fake listener in unit tests (the v1 test decrypts with the
cryptography package) and then against a real NuVotifier 2.7.3 on Paper 1.21.11:
- v2 with the right token:
OK - v2 with a wrong token:
Signature is not valid (invalid token?) - v1 with the public key: the console logs
Got a protocol v1 vote record
The whole script is about 200 lines and MIT licensed:
https://github.com/nickynice1/bestmcserverlist/tree/main/votifier-tester
Disclosure: this is written by the team behind BestMCServerList, a Minecraft server list,
and the script came out of testing our own vote delivery. The code and this article were written
with the help of an AI assistant; every result shown here was actually run and checked.
