mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 00:14:00 +00:00
fix: verify signed assignment rosters
This commit is contained in:
@@ -132,16 +132,16 @@ func SaveAssignments(ctx context.Context, db *sql.DB, assignments []DurableAssig
|
||||
// SaveVerifiedAssignmentRoster converts the backend-verified signed roster to
|
||||
// player-scoped rows. It rechecks the claims at this persistence boundary so a
|
||||
// caller cannot accidentally publish a token for another match or slot.
|
||||
func SaveVerifiedAssignmentRoster(ctx context.Context, db *sql.DB, assignment domain.Assignment, roster []domain.SignedJoinAuthorisation) error {
|
||||
if assignment.Allocation.State != domain.ServerAllocated || len(roster) == 0 {
|
||||
func SaveVerifiedAssignmentRoster(ctx context.Context, db *sql.DB, assignment domain.Assignment, roster []domain.SignedJoinAuthorisation, verify func([]byte, []byte) bool) error {
|
||||
if assignment.Allocation.State != domain.ServerAllocated || len(roster) == 0 || verify == nil {
|
||||
return fmt.Errorf("invalid verified assignment roster")
|
||||
}
|
||||
digest := domain.ManifestDigest(assignment.Manifest)
|
||||
rows := make([]DurableAssignment, 0, len(roster))
|
||||
for _, signed := range roster {
|
||||
auth := signed.Authorisation
|
||||
if len(signed.Signature) == 0 || auth.MatchID != assignment.Allocation.MatchID || auth.ServerID != assignment.Allocation.ServerID || auth.Protocol != strconv.Itoa(assignment.Allocation.Protocol) || auth.PlayerID == "" || auth.Slot < 0 || auth.Slot > 5 || auth.ExpiresAt.IsZero() {
|
||||
return fmt.Errorf("invalid signed assignment roster")
|
||||
if err := validateSignedRosterEntry(assignment, signed, verify); err != nil {
|
||||
return err
|
||||
}
|
||||
envelope, err := json.Marshal(signed)
|
||||
if err != nil {
|
||||
@@ -159,6 +159,14 @@ func SaveVerifiedAssignmentRoster(ctx context.Context, db *sql.DB, assignment do
|
||||
return SaveAssignments(ctx, db, rows)
|
||||
}
|
||||
|
||||
func validateSignedRosterEntry(assignment domain.Assignment, signed domain.SignedJoinAuthorisation, verify func([]byte, []byte) bool) error {
|
||||
auth := signed.Authorisation
|
||||
if len(signed.Signature) == 0 || verify == nil || !verify(domain.JoinAuthorisationBytes(auth), signed.Signature) || auth.MatchID != assignment.Allocation.MatchID || auth.ServerID != assignment.Allocation.ServerID || auth.Protocol != strconv.Itoa(assignment.Allocation.Protocol) || auth.PlayerID == "" || auth.Slot < 0 || auth.Slot > 5 || auth.ExpiresAt.IsZero() {
|
||||
return fmt.Errorf("invalid signed assignment roster")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetAssignment(ctx context.Context, db *sql.DB, playerID, matchID string, now time.Time) (DurableAssignment, error) {
|
||||
if db == nil || playerID == "" || matchID == "" || now.IsZero() {
|
||||
return DurableAssignment{}, fmt.Errorf("invalid assignment recovery arguments")
|
||||
|
||||
@@ -39,7 +39,22 @@ func TestAssignmentStoreRejectsInvalidBatches(t *testing.T) {
|
||||
if err := SaveAssignments(nil, nil, []DurableAssignment{{MatchID: "match-1", PlayerID: "player-1"}}); err == nil {
|
||||
t.Fatal("invalid assignment batch accepted")
|
||||
}
|
||||
if err := SaveVerifiedAssignmentRoster(nil, nil, domain.Assignment{}, nil); err == nil {
|
||||
if err := SaveVerifiedAssignmentRoster(nil, nil, domain.Assignment{}, nil, nil); err == nil {
|
||||
t.Fatal("empty verified roster accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignedRosterRequiresCryptographicVerification(t *testing.T) {
|
||||
now := time.Unix(1000, 0)
|
||||
assignment := domain.Assignment{Allocation: domain.Allocation{AllocationID: "allocation-1", MatchID: "match-1", ServerID: "server-1", Region: "EU", Build: "build-1", Protocol: 1, Transport: "enet", State: domain.ServerAllocated}, Manifest: domain.AllocationManifest{AllocationID: "allocation-1", MatchID: "match-1", ServerID: "server-1", Region: "EU", Build: "build-1", Protocol: 1, Transport: "enet", RosterDigest: "roster-1"}, Endpoint: "127.0.0.1:7777"}
|
||||
auth := domain.JoinAuthorisation{MatchID: "match-1", ServerID: "server-1", PlayerID: "player-1", SteamID: "steam-1", Slot: 0, Team: 0, Protocol: "1", Generation: 1, ExpiresAt: now.Add(time.Minute)}
|
||||
signed := domain.SignedJoinAuthorisation{Authorisation: auth, Signature: []byte("signature")}
|
||||
if err := validateSignedRosterEntry(assignment, signed, func([]byte, []byte) bool { return false }); err == nil {
|
||||
t.Fatal("forged signature accepted")
|
||||
}
|
||||
if err := validateSignedRosterEntry(assignment, signed, func(message, signature []byte) bool {
|
||||
return string(message) == string(domain.JoinAuthorisationBytes(auth)) && string(signature) == "signature"
|
||||
}); err != nil {
|
||||
t.Fatalf("valid signature rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user