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.
Top comments (0)