mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +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:
@@ -15,9 +15,14 @@ type SignedJoinAuthorisation struct {
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
// JoinAuthorisationBytes is the canonical claim encoding. KeyID is appended
|
||||
// last and is covered by the signature, so an attacker cannot redirect an
|
||||
// authorisation at a different key than the one that signed it. Game/scripts/
|
||||
// match_net.gd builds the identical byte sequence; the two must change
|
||||
// together.
|
||||
func JoinAuthorisationBytes(auth JoinAuthorisation) []byte {
|
||||
return []byte(fmt.Sprintf("%s\x00%s\x00%s\x00%s\x00%d\x00%d\x00%s\x00%d\x00%s",
|
||||
auth.MatchID, auth.ServerID, auth.PlayerID, auth.SteamID, auth.Slot, auth.Team, auth.Protocol, auth.Generation, auth.ExpiresAt.UTC().Format(time.RFC3339Nano)))
|
||||
return []byte(fmt.Sprintf("%s\x00%s\x00%s\x00%s\x00%d\x00%d\x00%s\x00%d\x00%s\x00%s",
|
||||
auth.MatchID, auth.ServerID, auth.PlayerID, auth.SteamID, auth.Slot, auth.Team, auth.Protocol, auth.Generation, auth.ExpiresAt.UTC().Format(time.RFC3339Nano), auth.KeyID))
|
||||
}
|
||||
|
||||
func SignJoinAuthorisation(auth JoinAuthorisation, sign func([]byte) ([]byte, error)) (SignedJoinAuthorisation, error) {
|
||||
@@ -34,6 +39,8 @@ func SignJoinAuthorisation(auth JoinAuthorisation, sign func([]byte) ([]byte, er
|
||||
// SignJoinAuthorisationHMAC is the interoperable production profile used by
|
||||
// the Godot allocated server. The key is mounted out-of-band; the signed
|
||||
// bytes remain the same canonical claim bytes used by the generic signer.
|
||||
// The caller must have set auth.KeyID to the ID of this key, so the verifier
|
||||
// can pick the right one out of its key set.
|
||||
func SignJoinAuthorisationHMAC(auth JoinAuthorisation, key []byte) (SignedJoinAuthorisation, error) {
|
||||
if len(key) == 0 {
|
||||
return SignedJoinAuthorisation{}, ErrJoinAuthorisation
|
||||
|
||||
Reference in New Issue
Block a user