From 86cb9c150b5cc9aa99be2d513d0a0e0eaf68abba Mon Sep 17 00:00:00 2001 From: sususu98 Date: Fri, 29 May 2026 12:17:25 +0800 Subject: [PATCH] feat(signature): upgrade provider signature checks --- internal/signature/claude_validation.go | 34 ++++++++++++++++ internal/signature/provider_compatibility.go | 20 +++++++++- .../signature/provider_compatibility_test.go | 40 ++++++++++++++++++- 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/internal/signature/claude_validation.go b/internal/signature/claude_validation.go index 4bad747e..a44f741b 100644 --- a/internal/signature/claude_validation.go +++ b/internal/signature/claude_validation.go @@ -226,6 +226,40 @@ func NormalizeClaudeThinkingSignature(rawSignature string, opts ...ClaudeSignatu } } +// NormalizeClaudeProviderNativeThinkingSignature strips any cache prefix, +// validates the signature, and returns the single-layer E-form expected by +// Claude-native providers. +func NormalizeClaudeProviderNativeThinkingSignature(rawSignature string, opts ...ClaudeSignatureValidationOptions) (string, error) { + opt := claudeSignatureValidationOptions(opts) + sig := stripClaudeSignaturePrefix(rawSignature) + if sig == "" { + return "", fmt.Errorf("empty signature") + } + + if len(sig) > MaxClaudeThinkingSignatureLen { + return "", fmt.Errorf("signature exceeds maximum length (%d bytes)", MaxClaudeThinkingSignatureLen) + } + + switch sig[0] { + case 'E': + if err := validateClaudeSingleLayerSignature(sig, opt); err != nil { + return "", err + } + return sig, nil + case 'R': + if err := validateClaudeDoubleLayerSignature(sig, opt); err != nil { + return "", err + } + decoded, err := base64.StdEncoding.DecodeString(sig) + if err != nil { + return "", fmt.Errorf("invalid double-layer signature: base64 decode failed: %w", err) + } + return string(decoded), nil + default: + return "", fmt.Errorf("invalid signature: expected 'E' or 'R' prefix, got %q", string(sig[0])) + } +} + func validateClaudeDoubleLayerSignature(sig string, opt ClaudeSignatureValidationOptions) error { decoded, err := base64.StdEncoding.DecodeString(sig) if err != nil { diff --git a/internal/signature/provider_compatibility.go b/internal/signature/provider_compatibility.go index 6cdb896f..885a92e9 100644 --- a/internal/signature/provider_compatibility.go +++ b/internal/signature/provider_compatibility.go @@ -229,6 +229,24 @@ func CompatibleSignatureForProviderBlock(targetProvider SignatureProvider, rawSi 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 +} + func normalizeSignatureTargetProvider(provider SignatureProvider) SignatureProvider { switch provider { case SignatureProviderGeminiBypass: @@ -255,7 +273,7 @@ func normalizeCompatibleSignatureForProvider(targetProvider SignatureProvider, r payload := SignaturePayloadWithoutProviderPrefix(rawSignature) switch normalizeSignatureTargetProvider(targetProvider) { case SignatureProviderClaude: - normalized, err := NormalizeClaudeThinkingSignature(payload) + normalized, err := NormalizeClaudeProviderNativeThinkingSignature(payload) if err != nil { return "" } diff --git a/internal/signature/provider_compatibility_test.go b/internal/signature/provider_compatibility_test.go index 5768d11c..dcb5b829 100644 --- a/internal/signature/provider_compatibility_test.go +++ b/internal/signature/provider_compatibility_test.go @@ -61,6 +61,42 @@ func TestDetectSignatureProvider_Gemini3EPrefixDoesNotLookClaude(t *testing.T) { } } +func TestCompatibleSignatureForProvider_ClaudeUsesProviderNativeEForm(t *testing.T) { + nativeSig := testClaudeThinkingSignature() + doubleEncoded := base64.StdEncoding.EncodeToString([]byte(nativeSig)) + + normalized, ok := CompatibleSignatureForProvider(SignatureProviderClaude, doubleEncoded) + if !ok { + t.Fatal("double-layer Claude signature should be compatible") + } + if normalized != nativeSig { + t.Fatalf("CompatibleSignatureForProvider(Claude) = %q, want provider-native %q", normalized, nativeSig) + } +} + +func TestCompatibleAntigravityClaudeThinkingSignature_UsesDoubleLayerRForm(t *testing.T) { + nativeSig := testClaudeThinkingSignature() + expected := base64.StdEncoding.EncodeToString([]byte(nativeSig)) + + normalized, ok := CompatibleAntigravityClaudeThinkingSignature(nativeSig) + if !ok { + t.Fatal("Claude signature should be compatible with Antigravity Claude") + } + if normalized != expected { + t.Fatalf("CompatibleAntigravityClaudeThinkingSignature = %q, want %q", normalized, expected) + } +} + +func TestCompatibleAntigravityClaudeThinkingSignature_RejectsGeminiEPrefix(t *testing.T) { + geminiSig := testGemini3ThoughtSignature([]byte{0x01, 0x0c, 0x39, 0xd6, 0xc7, 0x34}) + if !strings.HasPrefix(geminiSig, "E") { + t.Fatalf("test signature should start with E, got %q", geminiSig[:1]) + } + if normalized, ok := CompatibleAntigravityClaudeThinkingSignature(geminiSig); ok || normalized != "" { + t.Fatalf("Gemini E-prefix signature normalized=%q ok=%v, want rejected", normalized, ok) + } +} + func TestDetectSignatureProvider_DoesNotClassifyArbitraryBase64AsGemini(t *testing.T) { opaque := testGeminiThoughtSignature([]byte{0x45, 0x12}) if got := DetectSignatureProvider(opaque); got != SignatureProviderUnknown { @@ -172,9 +208,9 @@ func TestSanitizeClaudeMessagesSignaturesForModel_NormalizesSameProviderClaude(t nativeSig := testClaudeThinkingSignature() sig := "claude#" + nativeSig input := []byte(`{"model":"claude-sonnet","messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"keep","signature":"` + sig + `"},{"type":"text","text":"answer"}]}]}`) - expectedSig, err := NormalizeClaudeThinkingSignature(nativeSig) + expectedSig, err := NormalizeClaudeProviderNativeThinkingSignature(nativeSig) if err != nil { - t.Fatalf("NormalizeClaudeThinkingSignature failed: %v", err) + t.Fatalf("NormalizeClaudeProviderNativeThinkingSignature failed: %v", err) } output, report := SanitizeClaudeMessagesSignaturesForModel(input, "claude-sonnet-4-5")