Repository navigation
feat(modelkey): ctxm_ model-key format — Base62 body, CRC32 checksum, hash and hint (W01-2) - #82
Conversation
… hash and hint (W01-2) Wave W01-2 of the OpenAI-compatible API layer (RC-B): the new stdlib-only leaf package internal/modelkey defines the credential format of the model API. No caller outside the package yet, no migration, no live effect. - Format: "ctxm_" + 43 Base62 body characters (256 bits from crypto/rand, rejection sampling: bytes >= 248 are discarded) + 6 Base62 checksum characters (CRC32-IEEE over prefix and body, big-endian, '0'-padded) = 54 bytes. - Parse accepts only the canonical spelling: no trimming, the checksum is compared as text (no second spelling such as CRC+2^32), and every defect returns the one sentinel ErrShape. - Hash = hex(sha256(key)) like knowledge keys; HashPrefix = its first 8 bytes (failure-limiter key for W01-6); Hint = "ctxm_…" + the last four characters, all of them checksum characters. - Deviation from design/01 §4.1: String() returns the hint, not the plaintext; Plaintext() is the one method that returns the secret; GoString and LogValue return the hint too, and the plaintext sits behind a pointer, so fmt's reflection paths (non-string verbs, unexported struct fields) print an address instead of the key. - depguard rule modelkey-leaf (leaf rule 7/7): stdlib only, plus explicit denies for math/rand and math/rand/v2 (a seeded ChaCha8 passes every black-box test). 14 lines in deadcode-testonly-allow.txt (waiting for W01-4, W01-5, W02-05; three implicit hint-rendering methods). docs/security.md "Model API key format". Gates red -> green: - compile: `go test ./internal/modelkey/` "directory not found", then "undefined: Prefix" with the tests alone -> ok - single-character flips: a Parse without the checksum comparison accepts 2989 of 3299 flips -> 0 of 3299 - aliased checksum: a uint32-decoding Parse accepts CRC+2^32 -> rejected - shape with a recomputed checksum: Parse without the prefix check, without the character check, or with "_" allowed accepts ctxt_/CTXM_/NUL/0xFF/_ inputs -> rejected - rejection sampling: b % 62 without rejection gives the body "012345670123…" and chi² ≈ 28 000 over 10^5 keys -> golden body, chi² ≈ 50 (bound 150) - leak gate: the design's String() leaks the body on 10 of 14 paths, a string field with hint methods on 3 -> 0; hint output pinned exactly - depguard: an overlay import of internal/util, and Generate drawing from math/rand/v2, lint clean without the rule -> refused by modelkey-leaf - deadcode testonly: 14 NEW unreachable symbols -> ok (150 entries) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… a valid checksum (W01-2, review 2 F-1)
Second-review finding F-1 (low, test gap, no production defect): the single
character-check probe with a recomputed checksum sat at body position 20, so
a Parse loop that skips the first or the last body position survived the
whole suite and 20 s of fuzzing — every other flip failed at the CRC
comparison first.
- TestParseRejectsInvalidCharAtEveryBodyPosition: positions 5…47 × the 194
bytes outside [0-9A-Za-z], each with the checksum recomputed over prefix
and body (8342 inputs) — the refusal can only come from the character
check.
- FuzzParse: every accepted input must also match ^ctxm_[0-9A-Za-z]{49}$, an
oracle spelled independently of alphabet and isBase62.
- withSum is now a shared test helper.
- docs/security.md: the reflection output is an address in braces or a
%!verb(*string=0x…) marker (review 2 F-5, wording only).
Red -> green (mutants on Parse, full package suite):
- loop start len(Prefix)+1: ok -> FAIL TestParseRejectsInvalidCharAtEveryBodyPosition,
"accepted 194 of 8342 invalid-character inputs (want 0 of 8342)"
(first line "byte 0x00 at 5 with a valid checksum: err = <nil>")
- loop end Len-sumLen-1: ok -> FAIL, same test, "accepted 194 of 8342",
first line "byte 0x00 at 47 with a valid checksum: err = <nil>"
- loop end Len-1 stays green: an equivalent mutant — position 53 is a
checksum digit, checksum() emits only Base62, so a foreign byte there
always fails the string comparison.
- unmutated: go test -short -race -count=1 ./internal/modelkey/ ok; lint
both forms 0 issues; deadcode both ok (1 / 150 entries); fuzz 20 s PASS.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
There was a problem hiding this comment.
Note
Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.
🔵 Needs a closer look
This change introduces a new security-sensitive credential/key format (ctxm_) along with linting/policy updates, so it warrants a focused human security review to confirm leak-safety, format correctness, and interoperability with planned callers.
No open issues 🎉
What changed in this PR
Adds a new stdlib-only leaf Go package internal/modelkey defining the external-facing ctxm_ model API key format (Base62 body + CRC32 checksum), along with hashing/hinting utilities, extensive validation/leak-prevention tests, and supporting repo guardrails/documentation for staged rollout.
Changes:
- Implement
ctxm_key generation/parsing, SHA-256 hashing (and prefix), and hint-only rendering/logging behavior (String/GoString/LogValue). - Add comprehensive tests (golden vectors, mutation/alias rejection, uniformity gates, fuzzing, formatting/logging/JSON leak checks).
- Update
docs/security.mdwith key-format + secret-scanning guidance; add depguard leaf rule and deadcode-testonly allowlist entries.
| File | Summary |
|---|---|
go/internal/modelkey/modelkey.go |
Implements the ctxm_ model key type, parsing/generation, hashing, and hint-safe rendering/logging behavior. |
go/internal/modelkey/modelkey_test.go |
Adds golden-vector, rejection/mutation, uniformity, leak-prevention, and fuzz tests for model keys. |
go/deadcode-testonly-allow.txt |
Temporarily allowlists the new leaf API surface until downstream waves introduce production callers. |
docs/security.md |
Documents the model API key format and secret-scanning guidance for ctxm_… keys. |
.golangci.yml |
Adds a depguard rule enforcing internal/modelkey as a stdlib-only leaf (explicitly denies math/rand imports). |
Review effort: Balanced
Summary
W01-2 (RC-B, first wave of the credential class): new stdlib-only leaf package
internal/modelkey— thectxm_model-key format external consumers will present to the OpenAI-compatible/v1surface. No caller yet; the store (W01-3), the knowledge-path refusal (W01-4) andModelAuth(W01-6) land in later waves.ctxm_+ 43 Base62 body (256 bits fromcrypto/rand, rejection sampling, bytes ≥ 248 discarded) + 6 Base62 CRC32-IEEE checksum (big-endian,0-padded).Parseaccepts only the canonical spelling and compares the checksum as text (no second spelling of the same CRC); every defect is the oneErrShape.Hash= SHA-256 hex (same procedure as knowledge keys, fits the migration-161 CHECK),HashPrefix(limiter key for W01-6),Hint=ctxm_…+ last 4 checksum characters,HasPrefix(the knowledge paths' switch).String(),GoString()andLogValue()return the hint,Plaintext()is the only way to the secret, the plaintext sits behind a pointer sofmtby reflection prints an address, not the body;json.Marshalyields{}.modelkey-leaf(stdlib only;math/randandmath/rand/v2denied explicitly), 14 deadcode-testonly lines with their consuming wave,docs/security.mdsection "Model API key format" incl. secret-scanning pattern.Gates (red against 6773d5b, then green)
no Go files→ ok.Parsewithout the checksum compare accepts 2 989 → 0.b % 62without discard χ² ≈ 28 000 → ≈ 50 (limit 150); stepped reader with bytes 248–255 never yields a digit.%v %+v %#v %s %q %x, slog text/JSON, json): designString()leaks on 10 of 14 → 0.0KG44D/ctxm_…G44Drecomputed independently with Python.reflectpath shows the body; 18/20 mutants killed. One Low: two loop-boundary mutants survived → second commit adds a per-position invalid-character probe with a valid checksum (red: 194 of 8 342 accepted at positions 5 and 47 with the mutants, green after).go test -short -race ./...(66 ok;internal/llmTestChatBodyCapis a known host-load flake, package untouched).Hand-offs
HashPrefixas limiter key.HasPrefixdoes not see/before the prefix — such input goes down the knowledge path (one query, 401); no bypass.Wave report
.project/plan-openai-api-2026-11-03/reports/bau/W01-2.md, reviewreports/review/W01-2-fremdreview-2.md.🤖 Generated with Claude Code
Summary by CodeRabbit