Skip to content

Add TicketAuthority for reliable ownership-ticket answering - #4

Open
xib031 wants to merge 1 commit into
madoiscool:mainfrom
xib031:feat/ticket-authority
Open

xib031 wants to merge 1 commit into
madoiscool:mainfrom
xib031:feat/ticket-authority

Conversation

@xib031

@xib031 xib031 commented Oct 13, 2026 •

Copy link
Copy Markdown

Adds TicketAuthority: the ticket handler answers from validated local material, and one decision drives both the served bytes and the SteamID spoof.

Why

When a DRM-wrapped game launches, Steam asks the client for the app ownership ticket (IClientUser::GetAppOwnershipTicketExtendedData). In the old handler:

  • outside the Denuvo authorization window it read the registry AppTicket and, when that was empty, forged from a single cached blob (ConfigStore apptickets\7). If that blob was missing, it returned false and the handler wrote no reply; the DRM aborts the launch (the failure users report as error 54).
  • inside the authorization window it was store-only: no stored ticket meant no answer even though a forge source existed.
  • stored tickets were served without validation, so a stale blob (embedding another App ID) was answered as-is.
  • the SteamID spoof was decided separately from the served bytes, so the DRM's identity cross-checks could disagree even when a reply existed.

Concrete repro (before / after)

State: app configured via addappid; no registry AppTicket for it (HKCU\Software\Valve\Steam\Apps<id>\AppTicket absent); ConfigStore apptickets\7 gone (delete the key, or Steam -> Clear Download Cache).

  • before: store empty; the only forge source is apptickets\7; missing -> handler writes no reply -> DRM aborts.
  • after: discovery finds a usable base (a Lua-managed app's cached ticket, any credential-store ticket, or the client's own ticket) -> forged reply.

Same shape inside the Denuvo window with an ETicket but no AppTicket (the old store-only path answered nothing). And a stale stored ticket for another App ID: old served it as-is, new rejects it and forges instead.

What changed

TicketAuthority (src/Utils/Tickets/TicketAuthority.{h,cpp}) owns the decision; the handler call site is one TicketAuthority::Resolve(appId):

  • validated stored tickets first (embedded App ID, size, signature); invalid blobs fall through to forging and are kept only as a last-resort rung
  • the expected identity is derived once (stored ticket, stored SteamID credential, imports, or ETicket) and the SteamID spoof reads the same decision, so bytes and identity cannot disagree
  • forge bases are discovered: Lua-managed ids -> credential store ids (each probed in BOTH storage locations) -> the client's own ticket last
  • best-effort is the default: a reply is still produced while any usable signed bytes exist, and mixing is recorded; [tickets] identity_mode = "strict" refuses instead
  • optional ticket backend as the last resort; imports (setAppTicket / setETicket / redeem) validated at load time
  • one log line per decision (LOG_IPC_INFO); refusals carry the reason

Configuration

[tickets]
identity_mode = "best_effort" # or "strict"

Verification

  • built Release and Debug from this branch; exported surface unchanged (single TokeerUri export)
  • the full-featured fork build this work derives from was verified byte-identical (code sections) against the shipped DLLs during development

Notes

  • the diversion / startup work is split into a separate PR for independent review

@madoiscool

Copy link
Copy Markdown
Owner

@claude lock in

The ticket handler used to answer only when one cached blob existed
(ConfigStore apptickets\7) and otherwise returned without a reply; the
DRM then aborts the launch. Stored tickets were also served unvalidated,
and the SteamID spoof was decided independently of the bytes served.

TicketAuthority now owns the decision:
- validate stored tickets before serving (embedded App ID, size, signature)
- derive the expected identity once; the spoof reads the same decision
- discover forge bases instead of a fixed list: Lua ids, per-app
  credentials (both storage kinds), and the client ticket last
- best-effort default that records mixed_identity; strict mode optional
- optional backend fallback and import validation at load time
- log one line per decision
@xib031
xib031 force-pushed the feat/ticket-authority branch from cd3925c to b02ec4a Compare October 13, 2026 05:05
@xib031 xib031 changed the title Add TicketAuthority for reliable ownership-ticket answering + diversion startup change Add TicketAuthority for reliable ownership-ticket answering Oct 13, 2026
@xib031

xib031 commented Oct 13, 2026

Copy link
Copy Markdown
Author

All review points addressed:

Repro before/after is in the description.

One caveat: the vault stays in the fork build only (it fixed a real cache-clear case here). Not proposing it upstream unless it shows up on a stock build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants