package domain import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "sort" "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 } // 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\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) { 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 } // 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 } mac := hmac.New(sha256.New, key) _, _ = mac.Write(JoinAuthorisationBytes(auth)) return SignedJoinAuthorisation{Authorisation: auth, Signature: mac.Sum(nil)}, 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) } // AssignmentRosterDigest binds a manifest to the exact roster it was issued // with. Signing each authorisation individually proves each claim, but the // manifest also has to commit to the set, so a server cannot be handed a // truncated roster whose entries are each individually valid. // // Entries are hashed in slot order so the digest is independent of the order // the caller happened to build them in. func AssignmentRosterDigest(roster []SignedJoinAuthorisation) (string, error) { if len(roster) == 0 { return "", ErrJoinAuthorisation } ordered := make([]SignedJoinAuthorisation, len(roster)) copy(ordered, roster) sort.Slice(ordered, func(i, j int) bool { return ordered[i].Authorisation.Slot < ordered[j].Authorisation.Slot }) digest := sha256.New() for _, signed := range ordered { if signed.Authorisation.PlayerID == "" { return "", ErrJoinAuthorisation } digest.Write(JoinAuthorisationBytes(signed.Authorisation)) digest.Write([]byte{0}) } return hex.EncodeToString(digest.Sum(nil)), nil } // VerifyJoinAuthorisationHMAC builds the verifier the persistence boundary // re-checks each signature with, selecting the key named by the claim. Keys is // key ID to raw key; an unknown ID verifies as false rather than falling back // to any other key. func VerifyJoinAuthorisationHMAC(keys map[string][]byte) func([]byte, []byte) bool { return func(claims, signature []byte) bool { if len(keys) == 0 || len(claims) == 0 || len(signature) == 0 { return false } // The key ID is the last NUL-separated field of the canonical bytes. separator := bytes.LastIndexByte(claims, 0) if separator < 0 { return false } key, known := keys[string(claims[separator+1:])] if !known || len(key) == 0 { return false } mac := hmac.New(sha256.New, key) mac.Write(claims) return hmac.Equal(mac.Sum(nil), signature) } }