468 lines
20 KiB
Go
468 lines
20 KiB
Go
package signature
|
|
|
|
import "strings"
|
|
|
|
type SignatureProvider string
|
|
|
|
const (
|
|
SignatureProviderUnknown SignatureProvider = "unknown"
|
|
SignatureProviderClaude SignatureProvider = "claude"
|
|
SignatureProviderGemini SignatureProvider = "gemini"
|
|
SignatureProviderGeminiBypass SignatureProvider = "gemini_bypass"
|
|
SignatureProviderGPT SignatureProvider = "gpt"
|
|
// SignatureProviderKimi is identified by fixed signature size rather than by
|
|
// an envelope. See kimi_validation.go for the empirical basis and its limits.
|
|
SignatureProviderKimi SignatureProvider = "kimi"
|
|
// SignatureProviderGrok is a target-only family. DetectSignatureProvider never
|
|
// returns it: xAI emits no envelope, no version byte and no fixed length, and
|
|
// its ciphertext is statistically indistinguishable from uniform random bytes,
|
|
// so any positive claim would also capture every other opaque blob. Grok
|
|
// handling is provenance-first - establish the target from the model or route,
|
|
// then use InspectGrokEncryptedContent as a replay-safety shape check.
|
|
SignatureProviderGrok SignatureProvider = "grok"
|
|
)
|
|
|
|
type SignatureBlockKind string
|
|
|
|
const (
|
|
SignatureBlockKindUnknown SignatureBlockKind = "unknown"
|
|
SignatureBlockKindClaudeThinking SignatureBlockKind = "claude_thinking"
|
|
SignatureBlockKindGeminiModelPart SignatureBlockKind = "gemini_model_part"
|
|
SignatureBlockKindGeminiFunctionCall SignatureBlockKind = "gemini_function_call"
|
|
SignatureBlockKindGPTReasoning SignatureBlockKind = "gpt_reasoning"
|
|
)
|
|
|
|
type SignatureCompatibilityAction string
|
|
|
|
const (
|
|
SignatureActionPreserve SignatureCompatibilityAction = "preserve"
|
|
SignatureActionDropBlock SignatureCompatibilityAction = "drop_block"
|
|
SignatureActionDropSignature SignatureCompatibilityAction = "drop_signature"
|
|
SignatureActionReplaceWithGeminiBypass SignatureCompatibilityAction = "replace_with_gemini_bypass"
|
|
SignatureActionNoCompatibleReplacement SignatureCompatibilityAction = "no_compatible_replacement"
|
|
)
|
|
|
|
type SignatureCompatibilityDecision struct {
|
|
TargetProvider SignatureProvider
|
|
DetectedProvider SignatureProvider
|
|
BlockKind SignatureBlockKind
|
|
Compatible bool
|
|
Action SignatureCompatibilityAction
|
|
ReplacementSignature string
|
|
NormalizedSignature string
|
|
Reason string
|
|
}
|
|
|
|
// SignatureProviderFromModelName maps common model names to the provider family
|
|
// whose signed history can be safely replayed for that model.
|
|
func SignatureProviderFromModelName(modelName string) SignatureProvider {
|
|
lower := strings.ToLower(strings.TrimSpace(modelName))
|
|
switch {
|
|
case strings.Contains(lower, "claude"):
|
|
return SignatureProviderClaude
|
|
case strings.Contains(lower, "gemini"):
|
|
return SignatureProviderGemini
|
|
case strings.Contains(lower, "gpt"),
|
|
strings.Contains(lower, "openai"),
|
|
strings.Contains(lower, "codex"),
|
|
strings.HasPrefix(lower, "o1"),
|
|
strings.HasPrefix(lower, "o3"),
|
|
strings.HasPrefix(lower, "o4"):
|
|
return SignatureProviderGPT
|
|
case strings.Contains(lower, "kimi"),
|
|
strings.Contains(lower, "moonshot"),
|
|
strings.HasPrefix(lower, "k2"),
|
|
strings.HasPrefix(lower, "k3"):
|
|
return SignatureProviderKimi
|
|
case strings.Contains(lower, "grok"):
|
|
return SignatureProviderGrok
|
|
default:
|
|
return SignatureProviderUnknown
|
|
}
|
|
}
|
|
|
|
// selfDescribingSignatureFirstChars are the base64 first characters that a
|
|
// self-describing provider envelope can produce. A base64 first character is
|
|
// exactly the first payload byte shifted right by two, so a single character
|
|
// comparison rules out every known envelope without decoding anything:
|
|
//
|
|
// 'C' -> 0x08..0x0b : Claude CAIS (0x08)
|
|
// 'E' -> 0x10..0x13 : Claude single-layer (0x12), Gemini protobuf_field_2 (0x12)
|
|
// 'R' -> 0x44..0x47 : Claude double-layer R (0x45, inner 'E')
|
|
// 'g' -> 0x80..0x83 : GPT Fernet reasoning (0x80)
|
|
//
|
|
// Gemini's ascii_uuid envelope is deliberately absent. Its first byte is the
|
|
// first hex character of the UUID, which spreads over 'M', 'N', 'O', 'Y' and 'Z'
|
|
// depending on the value, and it is never a replay-safe envelope: it resolves to
|
|
// SignatureProviderUnknown whether or not it reaches the validators, and Gemini
|
|
// model parts recover it through the bypass sentinel keyed on block kind. Listing
|
|
// one of its five possible characters would only look like coverage.
|
|
//
|
|
// Any provider added here must also be validated in
|
|
// DetectSignatureProviderForBlock, otherwise its signatures would fall through
|
|
// to the residual class. TestSelfDescribingSignatureFirstChars_CoversEveryKnownEnvelope
|
|
// fails when a replay-safe envelope is missing from this set.
|
|
const selfDescribingSignatureFirstChars = "CERg"
|
|
|
|
// base64AlphabetSet builds a byte lookup table for the alphanumeric base64 core
|
|
// plus the alphabet-specific characters in extra. Signature charset validation
|
|
// runs over multi-kilobyte payloads, and a comparison chain over base64 text
|
|
// mispredicts on nearly every byte because the characters are effectively random;
|
|
// a single table load is branch-free and measures about an order of magnitude
|
|
// faster on the observed corpora.
|
|
func base64AlphabetSet(extra string) [256]bool {
|
|
var set [256]bool
|
|
for c := byte('A'); c <= 'Z'; c++ {
|
|
set[c] = true
|
|
}
|
|
for c := byte('a'); c <= 'z'; c++ {
|
|
set[c] = true
|
|
}
|
|
for c := byte('0'); c <= '9'; c++ {
|
|
set[c] = true
|
|
}
|
|
for i := 0; i < len(extra); i++ {
|
|
set[extra[i]] = true
|
|
}
|
|
return set
|
|
}
|
|
|
|
// maybeSelfDescribingSignatureEnvelope reports whether rawSignature can possibly
|
|
// be a self-describing provider envelope. It is a structural pre-filter, not a
|
|
// classifier: a false result is conclusive, a true result only narrows the
|
|
// candidate set. Opaque ciphertext that carries no envelope (xAI/Grok
|
|
// encrypted_content) is uniformly distributed over the byte space, so this
|
|
// rejects roughly 92% of it with one comparison and no allocation.
|
|
func maybeSelfDescribingSignatureEnvelope(rawSignature string) bool {
|
|
if rawSignature == "" {
|
|
return false
|
|
}
|
|
return strings.IndexByte(selfDescribingSignatureFirstChars, rawSignature[0]) >= 0
|
|
}
|
|
|
|
// DetectSignatureProvider classifies the provider family that can replay
|
|
// rawSignature. It intentionally uses Claude strict validation before Gemini
|
|
// detection because Gemini 3 signatures also decode from an E-prefixed base64
|
|
// string and can look Claude-like under shallow prefix checks.
|
|
func DetectSignatureProvider(rawSignature string) SignatureProvider {
|
|
return DetectSignatureProviderForBlock(rawSignature, SignatureBlockKindUnknown)
|
|
}
|
|
|
|
// DetectSignatureProviderForBlock classifies rawSignature with block-kind
|
|
// context. UUID-shaped payloads are deliberately not classified as replay-safe
|
|
// provider signatures; callers targeting Gemini should replace them with the
|
|
// bypass sentinel.
|
|
func DetectSignatureProviderForBlock(rawSignature string, blockKind SignatureBlockKind) SignatureProvider {
|
|
sig := strings.TrimSpace(rawSignature)
|
|
if sig == "" {
|
|
return SignatureProviderUnknown
|
|
}
|
|
|
|
if prefixedProvider, unprefixed, ok := SplitSignatureProviderPrefix(sig); ok {
|
|
switch prefixedProvider {
|
|
case SignatureProviderGemini:
|
|
if IsGeminiThoughtSignatureBypass(unprefixed) {
|
|
return SignatureProviderGeminiBypass
|
|
}
|
|
if isRecognizedGeminiProviderSignature(unprefixed, blockKind) {
|
|
return SignatureProviderGemini
|
|
}
|
|
case SignatureProviderClaude:
|
|
if IsValidClaudeThinkingSignature(unprefixed, ClaudeSignatureValidationOptions{Strict: true}) || IsValidClaudeCAISSignature(unprefixed) {
|
|
return SignatureProviderClaude
|
|
}
|
|
case SignatureProviderGPT:
|
|
if IsValidGPTReasoningSignature(unprefixed) {
|
|
return SignatureProviderGPT
|
|
}
|
|
}
|
|
return SignatureProviderUnknown
|
|
}
|
|
if strings.Contains(sig, "#") {
|
|
return SignatureProviderUnknown
|
|
}
|
|
|
|
// The bypass sentinel is a plain literal rather than an envelope, so it must
|
|
// be matched before the structural pre-filter below rejects it.
|
|
if IsGeminiThoughtSignatureBypass(sig) {
|
|
return SignatureProviderGeminiBypass
|
|
}
|
|
// Probes run from the strongest marker to the weakest:
|
|
// 1. GPT carries the literal "gAAAA" prefix, which pins both the version
|
|
// byte and the high timestamp bytes.
|
|
// 2. Claude CAIS carries marker 0x08 plus a literal "claude-" model text.
|
|
// 3. Claude single/double-layer carries marker 0x12 plus the same literal.
|
|
// 4. Gemini validates wire shape only and has no literal to anchor on, so
|
|
// it is the weakest judge and goes last.
|
|
//
|
|
// This ordering is defense in depth rather than a correctness requirement:
|
|
// Claude envelopes carry extra top-level fields beyond the container, which
|
|
// fails the single-record shape Gemini requires, so the two families stay
|
|
// separable in either order. TestGeminiEnvelopeNeverClaimsClaudeSignatures
|
|
// pins that invariant so a looser Gemini envelope check cannot make the
|
|
// order silently start mattering.
|
|
//
|
|
// The envelope pre-filter gates only the envelope probes. A blob that cannot
|
|
// be an envelope skips straight to the size probe below rather than returning
|
|
// early, because Kimi's uniformly distributed base64 starts with one of
|
|
// "CERg" about 6% of the time and would otherwise be dropped by whichever
|
|
// side of the gate it happened to land on.
|
|
if maybeSelfDescribingSignatureEnvelope(sig) {
|
|
if IsValidGPTReasoningSignature(sig) {
|
|
return SignatureProviderGPT
|
|
}
|
|
if IsValidClaudeCAISSignature(sig) {
|
|
return SignatureProviderClaude
|
|
}
|
|
if IsValidClaudeThinkingSignature(sig, ClaudeSignatureValidationOptions{Strict: true}) {
|
|
return SignatureProviderClaude
|
|
}
|
|
if isRecognizedGeminiProviderSignature(sig, blockKind) {
|
|
return SignatureProviderGemini
|
|
}
|
|
}
|
|
// Kimi carries no envelope, so it can only be claimed once every
|
|
// self-describing probe above has declined. Ordering it last means a length
|
|
// coincidence can never capture another provider's signature, and a future
|
|
// drift in Kimi's sizes costs Kimi its own identification rather than
|
|
// corrupting a neighbouring family.
|
|
if IsValidKimiThinkingSignature(sig) {
|
|
return SignatureProviderKimi
|
|
}
|
|
return SignatureProviderUnknown
|
|
}
|
|
|
|
func IsSignatureCompatibleWithProvider(targetProvider SignatureProvider, rawSignature string) bool {
|
|
decision := DecideSignatureCompatibility(targetProvider, rawSignature, SignatureBlockKindUnknown)
|
|
return decision.Compatible
|
|
}
|
|
|
|
// DecideSignatureCompatibility returns the safe handling policy for replaying a
|
|
// signed block into targetProvider.
|
|
func DecideSignatureCompatibility(targetProvider SignatureProvider, rawSignature string, blockKind SignatureBlockKind) SignatureCompatibilityDecision {
|
|
return DecideSignatureCompatibilityForModel(targetProvider, "", rawSignature, blockKind)
|
|
}
|
|
|
|
// DecideSignatureCompatibilityForModel returns the safe handling policy for replaying a
|
|
// signed block into targetProvider for targetModel.
|
|
func DecideSignatureCompatibilityForModel(targetProvider SignatureProvider, targetModel string, rawSignature string, blockKind SignatureBlockKind) SignatureCompatibilityDecision {
|
|
targetProvider = normalizeSignatureTargetProvider(targetProvider)
|
|
if blockKind == "" {
|
|
blockKind = SignatureBlockKindUnknown
|
|
}
|
|
|
|
detected := DetectSignatureProviderForBlock(rawSignature, blockKind)
|
|
decision := SignatureCompatibilityDecision{
|
|
TargetProvider: targetProvider,
|
|
DetectedProvider: detected,
|
|
BlockKind: blockKind,
|
|
}
|
|
|
|
if signatureProviderMatchesTarget(targetProvider, detected) {
|
|
decision.Compatible = true
|
|
decision.Action = SignatureActionPreserve
|
|
decision.NormalizedSignature = normalizeCompatibleSignatureForProvider(targetProvider, rawSignature, blockKind)
|
|
decision.Reason = claudeCompatibleSignatureReason(targetProvider, rawSignature, targetModel)
|
|
return decision
|
|
}
|
|
|
|
decision.Compatible = false
|
|
switch targetProvider {
|
|
case SignatureProviderGemini:
|
|
if blockKind == SignatureBlockKindGeminiFunctionCall || blockKind == SignatureBlockKindGeminiModelPart || blockKind == SignatureBlockKindUnknown {
|
|
decision.Action = SignatureActionReplaceWithGeminiBypass
|
|
decision.ReplacementSignature = GeminiSkipThoughtSignatureValidator
|
|
decision.Reason = "Gemini can bypass synthetic or incompatible model-part signatures with the documented sentinel"
|
|
return decision
|
|
}
|
|
decision.Action = SignatureActionDropBlock
|
|
decision.Reason = "signature is not compatible with Gemini and this block is not a bypass-safe Gemini model part"
|
|
case SignatureProviderClaude:
|
|
decision.Action = SignatureActionDropBlock
|
|
decision.Reason = "Claude has no cross-provider bypass sentinel for thinking blocks"
|
|
case SignatureProviderGPT:
|
|
decision.Action = SignatureActionDropBlock
|
|
decision.Reason = "GPT reasoning encrypted_content cannot be synthesized from another provider signature"
|
|
case SignatureProviderKimi:
|
|
// Kimi is the only target that can keep the reasoning text when the
|
|
// signature does not match. Its Messages endpoint never reads the field
|
|
// back: a mutated, truncated, non-base64 or absent signature all return
|
|
// 200, because reasoning continuity there travels in OpenAI-style
|
|
// reasoning_content instead. Dropping the block would discard recoverable
|
|
// thinking text for no upstream benefit, so drop only the signature.
|
|
decision.Action = SignatureActionDropSignature
|
|
decision.Reason = "Kimi does not validate replayed thinking signatures, so the block survives without one"
|
|
case SignatureProviderGrok:
|
|
// xAI decrypts encrypted_content and rejects the request with 400
|
|
// "Could not decrypt" when the blob is foreign or mutated, so a
|
|
// non-matching value has to leave with the block.
|
|
decision.Action = SignatureActionDropBlock
|
|
decision.Reason = "xAI verifies encrypted_content on replay and rejects foreign or mutated blobs"
|
|
default:
|
|
decision.Action = SignatureActionNoCompatibleReplacement
|
|
decision.Reason = "unknown target provider"
|
|
}
|
|
return decision
|
|
}
|
|
|
|
func SplitSignatureProviderPrefix(rawSignature string) (SignatureProvider, string, bool) {
|
|
prefix, rest, ok := strings.Cut(strings.TrimSpace(rawSignature), "#")
|
|
if !ok {
|
|
return SignatureProviderUnknown, rawSignature, false
|
|
}
|
|
provider := SignatureProviderFromCachePrefix(prefix)
|
|
if provider == SignatureProviderUnknown {
|
|
return SignatureProviderUnknown, rawSignature, false
|
|
}
|
|
return provider, strings.TrimSpace(rest), true
|
|
}
|
|
|
|
// SignatureProviderFromCachePrefix maps this repo's explicit provider-prefix
|
|
// envelope to a provider family. This is intentionally stricter than
|
|
// SignatureProviderFromModelName so arbitrary model names such as
|
|
// "claude-cache#..." cannot be mistaken for trusted provider provenance.
|
|
func SignatureProviderFromCachePrefix(prefix string) SignatureProvider {
|
|
switch strings.ToLower(strings.TrimSpace(prefix)) {
|
|
case "claude", "anthropic", "cais", "claude-cais", "claude_cais", "ccmax", "claude-code-max", "claude_code_max":
|
|
return SignatureProviderClaude
|
|
case "gemini", "google":
|
|
return SignatureProviderGemini
|
|
case "openai", "gpt", "codex":
|
|
return SignatureProviderGPT
|
|
default:
|
|
return SignatureProviderUnknown
|
|
}
|
|
}
|
|
|
|
// SignaturePayloadWithoutProviderPrefix strips this repo's provider cache prefix
|
|
// when present. The returned string is the value that should be replayed to an
|
|
// upstream provider.
|
|
func SignaturePayloadWithoutProviderPrefix(rawSignature string) string {
|
|
if _, unprefixed, ok := SplitSignatureProviderPrefix(rawSignature); ok {
|
|
return unprefixed
|
|
}
|
|
return strings.TrimSpace(rawSignature)
|
|
}
|
|
|
|
// CompatibleSignatureForProvider returns a replayable provider-native signature
|
|
// for targetProvider. It strips this repo's provider prefix and normalizes
|
|
// Claude signatures to the format expected by the target when possible.
|
|
func CompatibleSignatureForProvider(targetProvider SignatureProvider, rawSignature string) (string, bool) {
|
|
return CompatibleSignatureForProviderBlock(targetProvider, rawSignature, SignatureBlockKindUnknown)
|
|
}
|
|
|
|
// CompatibleSignatureForProviderBlock returns a replayable provider-native
|
|
// signature for targetProvider when the source block kind is known.
|
|
func CompatibleSignatureForProviderBlock(targetProvider SignatureProvider, rawSignature string, blockKind SignatureBlockKind) (string, bool) {
|
|
decision := DecideSignatureCompatibility(targetProvider, rawSignature, blockKind)
|
|
if !decision.Compatible || decision.NormalizedSignature == "" {
|
|
return "", false
|
|
}
|
|
return decision.NormalizedSignature, true
|
|
}
|
|
|
|
// CompatibleAntigravityClaudeThinkingSignature returns the double-layer R-form
|
|
// required by Antigravity Claude replay. It only accepts signatures that are
|
|
// strictly identifiable as Claude, so Gemini E-prefixed envelopes cannot slip
|
|
// through the looser Antigravity bypass normalization path.
|
|
func CompatibleAntigravityClaudeThinkingSignature(rawSignature string) (string, bool) {
|
|
if DetectSignatureProviderForBlock(rawSignature, SignatureBlockKindClaudeThinking) != SignatureProviderClaude {
|
|
return "", false
|
|
}
|
|
normalized, err := NormalizeClaudeThinkingSignature(
|
|
SignaturePayloadWithoutProviderPrefix(rawSignature),
|
|
ClaudeSignatureValidationOptions{Strict: true},
|
|
)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
return normalized, true
|
|
}
|
|
|
|
// claudeCompatibleSignatureReason explains why a matching signature is
|
|
// replayable. Claude CAIS signatures carry the issuing model inside the payload,
|
|
// so the embedded model and the target model are both reported to make signature
|
|
// decisions traceable in debug logs.
|
|
func claudeCompatibleSignatureReason(targetProvider SignatureProvider, rawSignature, targetModel string) string {
|
|
const genericReason = "signature provider matches target provider"
|
|
if targetProvider != SignatureProviderClaude {
|
|
return genericReason
|
|
}
|
|
info, err := InspectClaudeCAISSignature(SignaturePayloadWithoutProviderPrefix(rawSignature))
|
|
if err != nil {
|
|
return genericReason
|
|
}
|
|
reason := "valid Claude CAIS signature with embedded model " + info.ModelText + " is compatible with any Claude target"
|
|
if trimmedModel := strings.TrimSpace(targetModel); trimmedModel != "" {
|
|
reason += ", including target model " + trimmedModel
|
|
}
|
|
return reason
|
|
}
|
|
|
|
func normalizeSignatureTargetProvider(provider SignatureProvider) SignatureProvider {
|
|
switch provider {
|
|
case SignatureProviderGeminiBypass:
|
|
return SignatureProviderGemini
|
|
default:
|
|
return provider
|
|
}
|
|
}
|
|
|
|
func signatureProviderMatchesTarget(target, detected SignatureProvider) bool {
|
|
switch target {
|
|
case SignatureProviderGemini:
|
|
return detected == SignatureProviderGemini || detected == SignatureProviderGeminiBypass
|
|
case SignatureProviderClaude:
|
|
return detected == SignatureProviderClaude
|
|
case SignatureProviderGPT:
|
|
return detected == SignatureProviderGPT
|
|
case SignatureProviderKimi:
|
|
return detected == SignatureProviderKimi
|
|
default:
|
|
// SignatureProviderGrok is deliberately absent. Detection never yields it,
|
|
// so a Grok target must decide replay safety from provenance plus
|
|
// InspectGrokEncryptedContent rather than from a detected-provider match.
|
|
return false
|
|
}
|
|
}
|
|
|
|
func normalizeCompatibleSignatureForProvider(targetProvider SignatureProvider, rawSignature string, blockKind SignatureBlockKind) string {
|
|
payload := SignaturePayloadWithoutProviderPrefix(rawSignature)
|
|
switch normalizeSignatureTargetProvider(targetProvider) {
|
|
case SignatureProviderClaude:
|
|
if IsValidClaudeCAISSignature(payload) {
|
|
return payload
|
|
}
|
|
normalized, err := NormalizeClaudeProviderNativeThinkingSignature(payload)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return normalized
|
|
case SignatureProviderGemini:
|
|
if IsGeminiThoughtSignatureBypass(payload) {
|
|
return payload
|
|
}
|
|
if isRecognizedGeminiProviderSignature(payload, blockKind) {
|
|
return payload
|
|
}
|
|
case SignatureProviderGPT:
|
|
if IsValidGPTReasoningSignature(payload) {
|
|
return payload
|
|
}
|
|
case SignatureProviderKimi:
|
|
if IsValidKimiThinkingSignature(payload) {
|
|
return payload
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func isRecognizedGeminiProviderSignature(rawSignature string, blockKind SignatureBlockKind) bool {
|
|
if IsValidClaudeCAISSignature(rawSignature) {
|
|
return false
|
|
}
|
|
if IsValidGeminiThoughtSignature(rawSignature, GeminiThoughtSignatureValidationOptions{RequireKnownEnvelope: true}) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|