feat: compute cch via xxHash64 of full request body#192
feat: compute cch via xxHash64 of full request body#192Arkptz wants to merge 7 commits intogriffinmartin:mainfrom
Conversation
Replace hardcoded cch=fa690 SHA-256 test vectors with structural checks (5-char hex, not placeholder) since cch is now computed via xxHash64. Add xxhash64.ts to SOURCE_FILES in index.test.ts so temp-dir copies include the new module.
griffinmartin
left a comment
There was a problem hiding this comment.
Thanks for the careful reverse-engineering work — the xxHash64 implementation is clean, the seed-extraction story is convincing, and tests pass. Before merge, a few things must be addressed.
Must-fix
1. Rebase onto current main — branch is stale
mergeStateStatus: DIRTY,mergeable: CONFLICTING.- Conflict source is
src/transforms.ts:mainnow uses aprefixName(...)helper (PascalCase tool prefixing, PR #197) at lines 11/231/248; this PR still uses${TOOL_PREFIX}${tool.name}directly. - Releases 1.5.0 → 1.5.3 have shipped since this PR was opened.
2. Update ccVersion to current
main is at ccVersion: "2.1.112" (src/model-config.ts:13). This PR proposes 2.1.97, which is already two CC releases behind. Bump to whatever ships at merge time.
3. Re-verify CCH_SEED against Claude Code 2.1.112
The PR description states the seed is constant across v2.1.37 → v2.1.97. main is now 2.1.112. Please re-run scripts/extract-cch.ts against the current Claude Code release and confirm 0x6E52736AC806831En still produces matching hashes before merge. Reverse-engineered constants tied to a vendor binary need re-confirmation on every version bump.
4. Harden the placeholder replacement (src/transforms.ts:248)
const final = serialized.replace("cch=00000", `cch=${cch}`)Two real failure modes:
String.prototype.replace(string, …)only replaces the first occurrence. If user-supplied content (a prompt, a moved system entry, a tool result) ever contains the literalcch=00000, the wrong location is patched and the request fails validation while the log still records a plausible-lookingcch.- If the placeholder is missing entirely (e.g. an upstream caller bypasses
buildBillingHeaderValue, or a future transform strips the billing header),replaceis a silent no-op. The body goes out without a real cch butlog("transform_cch", { cch, … })still reports a value — false confidence.
Suggested fix: anchor on the trailing ; (cch=00000;) which is structurally part of the billing header format, and assert exactly one replacement occurred. Pseudo:
const PLACEHOLDER = "cch=00000;"
const idx = serialized.indexOf(PLACEHOLDER)
if (idx === -1 || serialized.indexOf(PLACEHOLDER, idx + 1) !== -1) {
log("transform_cch_placeholder_missing_or_duplicate", { ... })
return serialized // or throw, depending on policy
}
const final = serialized.slice(0, idx) + `cch=${cch};` + serialized.slice(idx + PLACEHOLDER.length)5. Decide on the computeCch export removal
This package is published (opencode-claude-auth@1.5.3, dist/index.d.ts). computeCch is exported from src/index.ts:51 on main and removed in this PR. Two acceptable resolutions:
- (preferred) Restore as a thin back-compat shim in
signing.ts/index.ts—computeCchis three lines, costs nothing, and external consumers may exist. - Otherwise, relabel the commit as
feat!:with aBREAKING CHANGE:footer per Conventional Commits, and let release-please bump the major.
The current feat: title with a silent public-API removal is the wrong combination.
Should-fix
6. Add a regression vector pinning CCH_SEED
src/xxhash64.test.ts covers algorithm correctness against the empty/seed-0 spec vector and computeCchHash's format/determinism — but nothing pins (body, expected_cch) against CCH_SEED. If the seed is ever changed by accident, the only thing that catches it is the manual scripts/extract-cch.ts (which requires the Claude CLI installed).
Add at least one fixture, e.g. capture a known body with the placeholder and assert the resulting cch matches a hard-coded value.
7. Strengthen xxhash64 spec coverage
The empty-input/seed-0 vector exercises only the avalanche + length-add paths. Add at least one of the standard xxHash test corpus vectors that cover:
- short non-empty input (<8 bytes)
- input crossing the 32-byte stripe boundary
This protects future maintainers against subtle off-by-one or stripe-loop regressions.
8. Mark cch as the last mutation in transformBody
At the end of transformBody (src/transforms.ts ~line 244):
// IMPORTANT: cch must be computed last. Any mutation of `parsed` or
// `serialized` after this point desyncs the hash from the body that
// will be sent on the wire.Cheap insurance; the function is long enough that someone will eventually want to add a step at the bottom.
Nits (non-blocking)
buildBillingHeaderValuenow produces a template (cch=00000;) rather than a final header. The doc comment you added is sufficient; if you're already touching the file, aTemplatesuffix in the name would make it clearer at call sites, but optional.scripts/extract-cch.tsuses hardcoded/tmp/...paths and has a benign race betweensetTimeout(..., 3000)andclearTimeout(timer)on child close. Dev-only script, not worth blocking on.
Summary of required actions
- Rebase onto
main(resolvetransforms.tsconflict againstprefixName). - Bump
ccVersionto current. - Re-run
scripts/extract-cch.tsagainst Claude Code 2.1.112 and confirm seed validity in a comment on this PR. - Anchor and validate the cch placeholder replacement; fail loudly if missing or duplicated.
- Restore
computeCchexport OR relabel commit asfeat!:. - Add at least one fixture-based cch regression vector.
- Add at least one short-input xxhash spec vector.
- Add the "must remain last" comment near the cch step.
The core change is good and matches my read of the Bun binary's behavior. Once items 1–5 land I'm happy to approve.
Summary
Replace SHA-256-based cch computation with xxHash64 of the full serialized request body, matching Claude Code's actual signing mechanism.
The previous implementation used
SHA-256(firstUserMessageText)[:5], but analysis of Claude Code's custom Bun binary reveals the actual algorithm:cch=00000placeholderxxHash64(bodyBytes, seed) & 0xFFFFF→ 5-char hexThe seed (
0x6E52736AC806831E) was extracted from Claude Code's Bun runtime and verified against the Python reference implementation across all input lengths. It has remained constant across versions (v2.1.37 → v2.1.97).Also includes:
ccVersionbump from2.1.90to2.1.97computeCch()function and its exportextract-cch) and prompt testing (test-prompt)Related issue
Related to #188, #190 — correct cch computation is a prerequisite for reliable billing validation.
Testing
extract-cch.tsintercept proxy confirms our hash matches real Claude CLI outputmake allpasses locallyChecklist
feat:,fix:,docs:,chore:, etc.)make allpasses locally (runs lint, build, and test)