mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
feat(join-auth): add key-ID rotation to signed join authorisations
Prerequisite for wiring the allocator to publish rosters. The signing key is a shared HMAC secret mounted into both the allocator and the allocated game server; without a key ID, rotating it would invalidate every authorisation already issued for an in-flight match, because a server holding only the new key cannot verify a token signed with the old one. Add KeyID to JoinAuthorisation and append it to the canonical claim bytes, so it is covered by the signature and cannot be repointed at a different key than the one that actually signed. Allocated servers now hold a set of currently-valid keys and select by ID: a rotation publishes the new key alongside the old, and the old is dropped once no live match can still reference it. The key file becomes a JSON map of key ID to base64 key. A file of raw key bytes is still accepted as a single key under the empty ID, which is what an unrotated deployment and the kind fixture use. Game/scripts/match_net.gd builds the canonical bytes independently, so it changes in lockstep; the cross-language golden token in test_match_net.gd is regenerated from the Go implementation and now carries a key ID. Added tests cover accepting either key mid-rotation, rejecting a retired key ID, and rejecting a token whose key ID was swapped to name a key the server does hold. Go suite and 223 Godot tests pass.
This commit is contained in:
@@ -31,12 +31,16 @@ import base64, hashlib, hmac, json, pathlib, sys, time
|
||||
|
||||
directory = pathlib.Path(sys.argv[1])
|
||||
key = b"compose-join-signing-key"
|
||||
key_id = "compose-key-1"
|
||||
expires = "2099-12-31T00:00:00Z"
|
||||
fields = ["compose-match-0001", "compose-server-0001", "compose-player", "compose-steam", "0", "0", "v1", "1", expires]
|
||||
# Field order and the trailing key ID must match
|
||||
# server/domain.JoinAuthorisationBytes and Game/scripts/match_net.gd.
|
||||
fields = ["compose-match-0001", "compose-server-0001", "compose-player", "compose-steam", "0", "0", "v1", "1", expires, key_id]
|
||||
canonical = b"\0".join(field.encode() for field in fields)
|
||||
signature = base64.urlsafe_b64encode(hmac.new(key, canonical, hashlib.sha256).digest()).rstrip(b"=").decode()
|
||||
envelope = {"Authorisation": {"MatchID": fields[0], "ServerID": fields[1], "PlayerID": fields[2], "SteamID": fields[3], "Slot": 0, "Team": 0, "Protocol": fields[6], "Generation": 1, "ExpiresAt": expires}, "Signature": signature}
|
||||
(directory / "join-signing-key").write_bytes(key)
|
||||
envelope = {"Authorisation": {"MatchID": fields[0], "ServerID": fields[1], "PlayerID": fields[2], "SteamID": fields[3], "Slot": 0, "Team": 0, "Protocol": fields[6], "Generation": 1, "ExpiresAt": expires, "KeyID": key_id}, "Signature": signature}
|
||||
# The key file maps key ID -> base64 key so a rotation can publish several.
|
||||
(directory / "join-signing-key").write_text(json.dumps({key_id: base64.b64encode(key).decode()}) + "\n")
|
||||
(directory / "join-roster.json").write_text(json.dumps([base64.urlsafe_b64encode(json.dumps(envelope, separators=(",", ":")).encode()).rstrip(b"=").decode()]) + "\n")
|
||||
PY
|
||||
|
||||
|
||||
Reference in New Issue
Block a user