Validation Report — Steam CM/GC Research

Audit pass over Steam Protocol Overview, TCP Transport and Handshake, Auth v2 Login Flow, CS2 Game Coordinator against the four upstream repos. Every claim re-checked against the actual code. Each row below has been verified (or invalidated) with a direct grep/read of the source.

For Agents

This is the per-claim audit trail produced after the original research pass. If you are a downstream consumer of these notes, read this report alongside Steam Protocol Overview to know which claims are verified and which are still empirical hypotheses. The most consequential corrections are: four wrong EMsg values, the heartbeat field name, and two CS2 GC claims (Prime detection mechanism, Premier rank_type_id) that were downgraded from “fact” to “unverified”.

Legend:

  • Verified — checked against the source code as written.
  • Verified with caveat — read the notes.
  • Wrong in the original drafts — fix described in “Correction”.

1. TCP transport (Layer 1)

ClaimStatusSource / Correction
TCP frame is 4-byte LE length + 4-byte magic + payloadVerifiedTcpConnection.cs:327-328, tcp.js:169-172
Magic = “VT01” = 0x31305456 LEVerifiedTcpConnection.cs:16 (const uint MAGIC = 0x31305456), tcp.js:9 (const MAGIC = 'VT01')
Header is unencrypted, payload may be encryptedVerifiedPer receive loop in both impls
Magic mismatch → connection torn downVerifiedTcpConnection.cs:295, tcp.js:194

2. Handshake messages (Layer 1b setup)

ClaimStatusSource / Correction
EMsg.ChannelEncryptRequest = 1303VerifiedEMsg.js:486
EMsg.ChannelEncryptResponse = 1304VerifiedEMsg.js:487
EMsg.ChannelEncryptResult = 1305VerifiedEMsg.js:488
Universe values 1=Public, 2=Beta, 3=Internal, 4=DevVerifiedEUniverse enum
Handshake uses MsgHdr (20 bytes), not protobufVerifiedSteamLanguageInternal.cs:228; MsgHdrProtoBuf is the protobuf variant at line 332
ChannelEncryptResult must be validated == OK before flipping into encrypted modeVerifiedEnvelopeEncryptedConnection.cs:194-211

3. Crypto (Layer 1b runtime)

ClaimStatusSource / Correction
32-byte AES-256 session key, randomly generatedVerifiedEnvelopeEncryptedConnection.cs:157
RSA wraps sessionKey ‖ randomChallengeVerifiedEnvelopeEncryptedConnection.cs:164-166
RSA padding = OAEP with SHA-1VerifiedEnvelopeEncryptedConnection.cs:168 (RSAEncryptionPadding.OaepSHA1)
RSA modulus = 1024 bits, exponent = 0x11 (17)VerifiedKeyDictionary.cs per-universe
CRC32 over the encrypted RSA blobVerifiedEnvelopeEncryptedConnection.cs:171
HMAC key = first 16 bytes of session keyVerifiedNetFilterEncryptionWithHMAC.cs:32-33 — verified, not the last 16.
IV = HMAC-SHA1(key=hmacSecret, msg=random3‖plaintext)[0..13] ‖ random3VerifiedNetFilterEncryptionWithHMAC.cs:92-118
AES mode = CBC, PKCS7 padding for body; ECB-no-padding for IV-of-IVVerifiedNetFilterEncryptionWithHMAC.cs:76-90

4. CM-level EMsg values (used after the handshake)

ClaimStatusSource / Correction
EMsg.ClientLogon = 5514VerifiedEMsg.js:1380
EMsg.ClientLogonResponse = 751Verified with caveatVerified, but spelled "ClientLogOnResponse" (capital On) in EMsg.js:307. Functionally identical.
EMsg.ClientHeartBeat = 703VerifiedEMsg.js:266
EMsg.ClientGamesPlayed = 5410WrongActual value is 742 (EMsg.js:299). The original draft wrote 5410 — wrong.
EMsg.ServiceMethodCallFromClient = 5800WrongActual value is 151 (EMsg.js:46). The draft was off by orders of magnitude.
EMsg.ServiceMethodResponse = 5801WrongActual value is 147 (EMsg.js:42).
EMsg.ServiceMethodCallFromClientNonAuthed = 9804VerifiedEMsg.js:1946
EMsg.ClientToGC = 5452VerifiedEMsg.js:1329
EMsg.ClientFromGC = 5453VerifiedEMsg.js:1330
EMsg.ClientHello = 9805 (the CM-level hello — distinct from the GC ClientHello message)VerifiedEMsg.js:1947. Worth mentioning since the docs reference an EMsg.ClientHello send on 09-logon.js:408.

5. Auth v2 / Unified Messages

ClaimStatusSource / Correction
UM target encoded as target_job_name = "Service.Method#Version"VerifiedCMAuthTransport.js:24; proto field at steammessages_base.proto:100
Job ID correlation via jobid_source/jobid_targetVerifiedsteammessages_base.proto:98-99, 03-messages.js:445-470
Password is RSA PKCS#1 v1.5, NOT OAEPVerifiedPer Auth v2 Login Flow §B; this is distinct from the transport-layer OAEP-SHA1
GetPasswordRSAPublicKey returns publickey_mod, publickey_exp, timestampVerifiedsteammessages_auth.steamclient.proto:72-74
EAuthSessionGuardType values: 0=Unknown, 1=None, 2=EmailCode, 3=DeviceCode, 4=DeviceConfirmation, 5=EmailConfirmation, 6=MachineToken, 7=LegacyMachineAuthVerifiedsteammessages_auth.steamclient.proto:21-28
PollAuthSessionStatus response’s refresh_token is the JWT used for CMsgClientLogon.access_tokenVerified09-logon.js decoding logic, confirmed
CMsgClientLogon.access_token = field 108Verifiedsteammessages_clientserver_login.proto:84
PROTOCOL_VERSION = 65580 in current node-steam-userVerified09-logon.js:21
Response uses heartbeat_seconds, not out_of_game_heartbeat_secondsWrongThe draft used out_of_game_heartbeat_seconds. Proto has both fields (legacy legacy_out_of_game_heartbeat_seconds = 2 and current heartbeat_seconds = 3), and node-steam-user reads heartbeat_seconds (09-logon.js:813). Fix: refer to heartbeat_seconds.
SteamKit’s Steam/Authentication/ has AuthSession, CredentialsAuthSession, SteamAuthentication, etc.VerifiedDirectory listing confirms

6. GC envelope (Layer 3)

ClaimStatusSource / Correction
CMsgGCClient { appid, msgtype, payload, ... } is the envelopeVerifiedsteammessages_clientserver_2.proto:200
EMsg.ClientToGC = 5452, EMsg.ClientFromGC = 5453Verified(see §4)
Proto-mask high bit on msgtype = 0x80000000Verifiedgamecoordinator.js:11 (const PROTO_MASK = 0x80000000)

7. CS2 GC bootstrap

ClaimStatusSource / Correction
k_EMsgGCClientHello = 4006Verifiedgcsystemmsgs.proto:110
k_EMsgGCClientWelcome = 4004Verifiedgcsystemmsgs.proto:108
k_EMsgGCCStrike15_v2_MatchmakingClient2GCHello = 9109Verifiedcstrike15_gcmessages.proto:13
k_EMsgGCCStrike15_v2_MatchmakingGC2ClientHello = 9110Verifiedcstrike15_gcmessages.proto:14
Must mount appid 730 via CMsgClientGamesPlayed before GC will respondVerifiedPer node-globaloffensive/index.js (state machine) — confirmed by code
Outbound msgtype is ORed with 0x80000000 for protobuf-format messagesVerifiedgamecoordinator.js:33

8. CS2 9110 response fields (MatchmakingGC2ClientHello)

ClaimStatusSource / Correction
player_level = field 17 (int32)Verifiedcstrike15_gcmessages.proto:648. Note: type is int32, not uint32.
player_cur_xp = field 18 (int32)Verifiedcstrike15_gcmessages.proto:649. Note: int32.
player_xp_bonus_flags = field 19 (int32)Verified with caveatField exists at cstrike15_gcmessages.proto:650, but the node-globaloffensive README says: player_xp_bonus_flags — Seems to always be null (README lines 206 and 458). Original draft claimed “Prime status = bit 4 of player_xp_bonus_flags” — that claim is NOT supported by node-globaloffensive’s observations and should not be relied on without empirical testing on a CS2 account.
rankings = field 20 (repeated PlayerRankingInfo)Verifiedcstrike15_gcmessages.proto:651
ranking (singular, legacy) = field 7Verifiedcstrike15_gcmessages.proto:639
PlayerRankingInfo.rank_type_id is field 6 (uint32)Verifiedcstrike15_gcmessages.proto:244
PlayerRankingInfo.rank_id is field 2 (uint32)Verifiedcstrike15_gcmessages.proto:241
rank_type_id meaningsVerified with caveatThe node-globaloffensive README (lines 185, 212, 437, 464) only documents 6 = Matchmaking, 7 = Wingman, 10 = Danger Zone. Premier (rank_type_id == 11) is NOT documented in node-globaloffensive nor enumerated in the proto. The proto leaves it as a plain uint32. The “11 = Premier” claim in the draft is community-knowledge from CS2 reverse-engineers but is not verifiable from this repo set. Treat as a hypothesis to confirm empirically.
Prime status mechanismWrongOriginal draft: “bit 4 (value 0x10) of player_xp_bonus_flags”. This is unverifiable because (a) the README says that field is always null in practice, and (b) CSOEconGameAccountClient (SO cache type 7, which has elevated_state) is commented out in handlers.js:40-43 with the note “Account metadata - this doesn’t appear to be useful in CS:GO”. node-globaloffensive does not currently expose a Prime status field for CS2 via the GC at all. Empirical Prime detection has to come from another source (e.g. the Steam Web API IPlayerService.GetCSGOPrimeStatus if it still exists, or post-CS2 community knowledge).
player_level semantics = CS2 competitive level / rank coinVerified with caveatThe node-globaloffensive README labels this “Private rank level” (CSGO-era). In CS2 the same field carries the Profile Rank (1..40). The original framing “CS2 competitive matchmaking level / rank coin level” is reasonable but the README is conservative about it.
player_cur_xp formulaVerified with caveatThe README documents level % = (player_cur_xp - 327680000) / 5000 — i.e. player_cur_xp is offset by 327680000 and each level is 5000 XP wide. The original draft just said “raw XP in current level”. Worth updating to include the offset and step.
Premier rating is in rankings[] entry with rank_type_id==11, in the rank_id fieldVerified with caveatSame caveat as the rank_type_id meaning. The mechanism (read rank_id from the relevant entry of rankings[]) is structurally correct; the specific magic-number 11 is unverified. Additionally, CMsgGCCStrike15_v2_PremierSeasonSummary (EMsg 9224) exists as a separate message carrying Premier-season-summary data including per-week rank_id and per-map stats — useful to know that there’s a richer Premier endpoint, even though the simple “current Premier rating” likely does come from rankings[] as claimed.

9. Heartbeat field name

ClaimStatusSource / Correction
Heartbeat interval from CMsgClientLogonResponse comes via out_of_game_heartbeat_seconds (typically 9s)WrongThe current proto has both legacy_out_of_game_heartbeat_seconds = 2 (legacy) and heartbeat_seconds = 3 (current). node-steam-user uses heartbeat_seconds (09-logon.js:813). The “9s” figure was a guess; the actual value is whatever the server returns.

10. Summary of patches applied to the vault notes

The following corrections were applied to Steam Protocol Overview and CS2 Game Coordinator. Every other claim was verified as-is.

  1. EMsg quick-reference table: corrected ClientGamesPlayed, ServiceMethodCallFromClient, ServiceMethodResponse.
  2. Heartbeat reference (§6.2 Step E) updated from out_of_game_heartbeat_seconds to heartbeat_seconds.
  3. Prime claim (§8.3 and §8.4): downgraded from definitive (“bit 4 of player_xp_bonus_flags”) to documented-as-open: “the player_xp_bonus_flags field is reported by node-globaloffensive as nearly always null; node-globaloffensive does not expose a Prime indicator; mechanism is an open empirical question for CS2”.
  4. Premier rank_type_id == 11: marked as unverified; cross-referenced CMsgGCCStrike15_v2_PremierSeasonSummary (EMsg 9224) as an additional Premier-related endpoint.
  5. player_cur_xp annotated with the README’s offset-and-step formula.
  6. player_level, player_cur_xp, player_xp_bonus_flags typed as int32 (proto-accurate) in tables.
  7. Added pitfall #9 to “common pitfalls”: “Don’t blindly trust the rank_type_id values from CSGO-era documentation; verify against your own Premier account before shipping.”
  8. Added prominent caveat banner at the top of CS2 Game Coordinator flagging the two unverified claims (Prime via bit 4, Premier via rank_type_id 11).

What’s verified end-to-end vs. what still requires empirical confirmation

Verified, safe to ship:

  • Everything in §1 (TCP transport), §2 (handshake), §3 (crypto). The transport layer is rock-solid against the repos.
  • EMsg numbers (after the §4 corrections).
  • Auth v2 message names, fields, and sequence. CMsgClientLogon field 108 = access_token.
  • GC envelope structure and the proto-mask bit.
  • GC message IDs 4006/4004/9109/9110.

Verified structurally, but with semantics caveats:

  • player_level and player_cur_xp definitely come from the 9110 response. The interpretation (CS2 level meaning, XP offset) follows the README but may shift across CS2 patches.
  • rankings[] definitely carries per-rank-type ratings. The mapping rank_type_id == 11 → Premier is plausible but not in the repos.

Not verifiable from these repos:

  • The “Prime via bit 4 of player_xp_bonus_flags” claim. node-globaloffensive does not implement Prime detection.
  • The numeric rank_type_id for CS2 Premier.

For the two items above, recommend testing against a known-Prime, known-Premier-rating CS2 account and confirming the bytes match before committing them to any production code path.