mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
feat: sign reconnect authorisations
This commit is contained in:
+3
-1
@@ -48,7 +48,9 @@ product policy are in [`docs/MATCHMAKING.md`](docs/MATCHMAKING.md).
|
||||
- [ ] **IN PROGRESS:** Validate Steam Web API tickets only in the secure backend;
|
||||
issue revocable sessions and reconnect-safe match/identity/slot authorisations
|
||||
with server-owned connection-generation fencing. Pure Go ticket/session and
|
||||
reconnect policies exist; production Steam/backend adapters remain.
|
||||
reconnect policies exist, including canonical signed join-authorisation
|
||||
issuance/verification; production Steam/backend adapters and persistent
|
||||
lease fencing remain.
|
||||
- [ ] **IN PROGRESS:** Authenticate results with pod/GameServer-bound workload
|
||||
identity; make identical duplicates idempotent and conflicting results
|
||||
inert/alerting. Pure Go credential-claim validation, binding, hashing,
|
||||
|
||||
+1
-1
@@ -1201,7 +1201,7 @@ the local/CI/community transport, not a silent production fallback.
|
||||
| 8.21 `[D:8.5,8.20]` | **IN PROGRESS.** Pure Go rating core implements canonical Glicko-2, daily inactivity, ranked 1/3 and casual 1/N human-opponent weights, deterministic opponent ordering, and authoritative draw/overtime/abandon scoring | `server/domain/rating.go` has canonical/inactivity/weight/invalid-input plus draw/OT/abandon fixtures; PostgreSQL snapshot locking, rating transaction integration, seasons and concurrent result transaction tests remain |
|
||||
| 8.22 `[D:8.21]` | **IN PROGRESS.** Pure Go ranked profile exposes the first ten games as provisional, derives tiers only through validated backend-owned rating bands, and keeps casual ratings outside the API; authenticated HTTP now returns the authoritative ranked view | `server/domain/rating.go`, `tier_test.go` and `server/api/service.go` cover provisional override, exact band boundaries, malformed policy rejection, session authentication and ranked-only response fields; persisted tier policy, client UI and reconnect transport remain |
|
||||
| 8.23 `[D:8.21]` | **IN PROGRESS.** Pure Go ranked-only season policy compresses 25% toward 1500, clamps RD to 200–350, preserves volatility/history, is idempotent by season ID, and defines exact 12-week windows/due detection; migration and Go store now persist a per-player/per-season marker and rating update atomically | `server/domain/rating.go`, `season_test.go`, `server/migrations/0001_initial.sql` and `server/store/season_sql.go` cover compression, floor/cap, duplicate replay, window boundary, completed-season idempotence, row locking and conflict-safe rollover markers; live PostgreSQL execution and maintenance scheduler remain |
|
||||
| 8.24 `[D:8.9,8.20,8.21]` | **IN PROGRESS.** Pure Go ranked connection policy binds match/server/player/team/slot/protocol, supports 60 s reclaim with server-owned generations, fences old connections, and applies the rolling 7-day 5 m/15 m/1 h/24 h abandon ladder | `server/domain/reconnect.go` covers repeated backend-independent reclaim, binding rejection, old-generation fencing, grace boundary and deterministic cooldown audit ordering; signed authorisations, persistent lease fencing, join transport and full match/result integration remain |
|
||||
| 8.24 `[D:8.9,8.20,8.21]` | **IN PROGRESS.** Pure Go ranked connection policy binds match/server/player/team/slot/protocol, supports 60 s reclaim with server-owned generations, fences old connections, and applies the rolling 7-day 5 m/15 m/1 h/24 h abandon ladder; canonical signed authorisation issuance/verification now gates admission | `server/domain/reconnect.go`, `join_auth.go` and adversarial fixtures cover repeated backend-independent reclaim, all signed claim binding, tampering, failed verification, old-generation fencing, grace boundary and deterministic cooldown audit ordering; persistent lease fencing, join transport and full match/result integration remain |
|
||||
| 8.25 `[D:8.10,8.24]` | **IN PROGRESS.** Pure Go result policy binds match/server/workload identity, hashes canonical payloads, makes identical retries idempotent, leaves conflicts inert, separates integrity eligibility, classifies roster/simulation/result/fairness evidence, validates annotation signatures/digests, and exposes 5 m alert/30 m review delivery thresholds; Go store SQL defines conflict-safe receipt insert, deterministic match/rating locks and atomic completion/outbox boundaries | `server/domain/result.go`, `server/domain/workload.go` and `server/store/result_sql.go` plus adversarial fixtures cover credential binding, duplicate/conflict, annotation forgery, delivery-outage-versus-integrity classification, commit and lock ordering; production credential verification, Agones annotation persistence/reconciliation, live PostgreSQL execution and integrity evidence adapters remain |
|
||||
|
||||
#### 8D — Agones, allocation and regional scaling
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SignedJoinAuthorisation is the transport envelope. The signing primitive is
|
||||
// supplied by the backend signer so this policy stays independent of key
|
||||
// storage and cryptographic algorithm choice.
|
||||
type SignedJoinAuthorisation struct {
|
||||
Authorisation JoinAuthorisation
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
|
||||
func SignJoinAuthorisation(auth JoinAuthorisation, sign func([]byte) ([]byte, error)) (SignedJoinAuthorisation, error) {
|
||||
if sign == nil {
|
||||
return SignedJoinAuthorisation{}, ErrJoinAuthorisation
|
||||
}
|
||||
signature, err := sign(JoinAuthorisationBytes(auth))
|
||||
if err != nil || len(signature) == 0 {
|
||||
return SignedJoinAuthorisation{}, ErrJoinAuthorisation
|
||||
}
|
||||
return SignedJoinAuthorisation{Authorisation: auth, Signature: append([]byte(nil), signature...)}, nil
|
||||
}
|
||||
|
||||
func (r *RankedConnections) AdmitSigned(signed SignedJoinAuthorisation, verify func([]byte, []byte) bool, now time.Time) (uint64, error) {
|
||||
if len(signed.Signature) == 0 || verify == nil || !verify(JoinAuthorisationBytes(signed.Authorisation), signed.Signature) {
|
||||
return 0, ErrJoinAuthorisation
|
||||
}
|
||||
return r.Admit(signed.Authorisation, now)
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -92,3 +94,42 @@ func TestRankedAbandonCooldownUsesRollingSevenDayLadder(t *testing.T) {
|
||||
t.Fatalf("abandonment repeated: %+v", again)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignedJoinAuthorisationBindsEveryClaimBeforeReclaim(t *testing.T) {
|
||||
now := time.Unix(1000, 0).UTC()
|
||||
r, err := NewRankedConnections("match-1", "server-1", "v1", testRoster(now))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key := []byte("test-signing-key")
|
||||
sign := func(payload []byte) ([]byte, error) {
|
||||
mac := hmac.New(sha256.New, key)
|
||||
_, _ = mac.Write(payload)
|
||||
return mac.Sum(nil), nil
|
||||
}
|
||||
verify := func(payload, signature []byte) bool {
|
||||
expected, _ := sign(payload)
|
||||
return hmac.Equal(expected, signature)
|
||||
}
|
||||
signed, err := SignJoinAuthorisation(testRoster(now)[0], sign)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gen, err := r.AdmitSigned(signed, verify, now); err != nil || gen != 1 {
|
||||
t.Fatalf("signed initial admit = %d, %v", gen, err)
|
||||
}
|
||||
if err := r.Disconnect("a", 1, now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tampered := signed
|
||||
tampered.Authorisation.Slot = 1
|
||||
if _, err := r.AdmitSigned(tampered, verify, now.Add(time.Second)); !errors.Is(err, ErrJoinAuthorisation) {
|
||||
t.Fatalf("tampered slot accepted: %v", err)
|
||||
}
|
||||
if _, err := r.AdmitSigned(signed, func([]byte, []byte) bool { return false }, now.Add(RankedReconnectGrace)); !errors.Is(err, ErrJoinAuthorisation) {
|
||||
t.Fatalf("unverified signature accepted: %v", err)
|
||||
}
|
||||
if gen, err := r.AdmitSigned(signed, verify, now.Add(RankedReconnectGrace)); err != nil || gen != 2 {
|
||||
t.Fatalf("signed reclaim = %d, %v", gen, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user