mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-04-26 05:57:49 +00:00
Add package-level comment documenting the protobuf tree structure, base64 encoding equivalence proof, output dimensions, and spec section references. Remove unreachable legacy_vertex_group dead code.
392 lines
12 KiB
Go
392 lines
12 KiB
Go
// Claude thinking signature validation for Antigravity bypass mode.
|
|
//
|
|
// Spec reference: SIGNATURE-CHANNEL-SPEC.md
|
|
//
|
|
// # Encoding Detection (Spec §3)
|
|
//
|
|
// Claude signatures use base64 encoding in one or two layers. The raw string's
|
|
// first character determines the encoding depth — this is mathematically equivalent
|
|
// to the spec's "decode first, check byte" approach:
|
|
//
|
|
// - 'E' prefix → single-layer: payload[0]==0x12, first 6 bits = 000100 = base64 index 4 = 'E'
|
|
// - 'R' prefix → double-layer: inner[0]=='E' (0x45), first 6 bits = 010001 = base64 index 17 = 'R'
|
|
//
|
|
// All valid signatures are normalized to R-form (double-layer base64) before
|
|
// sending to the Antigravity backend.
|
|
//
|
|
// # Protobuf Structure (Spec §4.1, §4.2) — strict mode only
|
|
//
|
|
// After base64 decoding to raw bytes (first byte must be 0x12):
|
|
//
|
|
// Top-level protobuf
|
|
// ├── Field 2 (bytes): container ← extractBytesField(payload, 2)
|
|
// │ ├── Field 1 (bytes): channel block ← extractBytesField(container, 1)
|
|
// │ │ ├── Field 1 (varint): channel_id [required] → routing_class (11 | 12)
|
|
// │ │ ├── Field 2 (varint): infra [optional] → infrastructure_class (aws=1 | google=2)
|
|
// │ │ ├── Field 3 (varint): version=2 [skipped]
|
|
// │ │ ├── Field 5 (bytes): ECDSA sig [skipped, per Spec §11]
|
|
// │ │ ├── Field 6 (bytes): model_text [optional] → schema_features
|
|
// │ │ └── Field 7 (varint): unknown [optional] → schema_features
|
|
// │ ├── Field 2 (bytes): nonce 12B [skipped]
|
|
// │ ├── Field 3 (bytes): session 12B [skipped]
|
|
// │ ├── Field 4 (bytes): SHA-384 48B [skipped]
|
|
// │ └── Field 5 (bytes): metadata [skipped, per Spec §11]
|
|
// └── Field 3 (varint): =1 [skipped]
|
|
//
|
|
// # Output Dimensions (Spec §8)
|
|
//
|
|
// routing_class: routing_class_11 | routing_class_12 | unknown
|
|
// infrastructure_class: infra_default (absent) | infra_aws (1) | infra_google (2) | infra_unknown
|
|
// schema_features: compact_schema (len 70-72, no f6/f7) | extended_model_tagged_schema (f6 exists) | unknown
|
|
// legacy_route_hint: only for ch=11 — legacy_default_group | legacy_aws_group | legacy_vertex_direct/proxy
|
|
//
|
|
// # Compatibility
|
|
//
|
|
// Verified against all confirmed spec samples (Anthropic Max 20x, Azure, Vertex,
|
|
// Bedrock) and legacy ch=11 signatures. Both single-layer (E) and double-layer (R)
|
|
// encodings are supported. Historical cache-mode 'modelGroup#' prefixes are stripped.
|
|
package claude
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache"
|
|
"github.com/tidwall/gjson"
|
|
"google.golang.org/protobuf/encoding/protowire"
|
|
)
|
|
|
|
const maxBypassSignatureLen = 8192
|
|
|
|
type claudeSignatureTree struct {
|
|
EncodingLayers int
|
|
ChannelID uint64
|
|
Field2 *uint64
|
|
RoutingClass string
|
|
InfrastructureClass string
|
|
SchemaFeatures string
|
|
ModelText string
|
|
LegacyRouteHint string
|
|
HasField7 bool
|
|
}
|
|
|
|
func ValidateClaudeBypassSignatures(inputRawJSON []byte) error {
|
|
messages := gjson.GetBytes(inputRawJSON, "messages")
|
|
if !messages.IsArray() {
|
|
return nil
|
|
}
|
|
|
|
messageResults := messages.Array()
|
|
for i := 0; i < len(messageResults); i++ {
|
|
contentResults := messageResults[i].Get("content")
|
|
if !contentResults.IsArray() {
|
|
continue
|
|
}
|
|
parts := contentResults.Array()
|
|
for j := 0; j < len(parts); j++ {
|
|
part := parts[j]
|
|
if part.Get("type").String() != "thinking" {
|
|
continue
|
|
}
|
|
|
|
rawSignature := strings.TrimSpace(part.Get("signature").String())
|
|
if rawSignature == "" {
|
|
return fmt.Errorf("messages[%d].content[%d]: missing thinking signature", i, j)
|
|
}
|
|
|
|
if _, err := normalizeClaudeBypassSignature(rawSignature); err != nil {
|
|
return fmt.Errorf("messages[%d].content[%d]: %w", i, j, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func normalizeClaudeBypassSignature(rawSignature string) (string, error) {
|
|
sig := strings.TrimSpace(rawSignature)
|
|
if sig == "" {
|
|
return "", fmt.Errorf("empty signature")
|
|
}
|
|
|
|
if idx := strings.IndexByte(sig, '#'); idx >= 0 {
|
|
sig = strings.TrimSpace(sig[idx+1:])
|
|
}
|
|
|
|
if sig == "" {
|
|
return "", fmt.Errorf("empty signature after stripping prefix")
|
|
}
|
|
|
|
if len(sig) > maxBypassSignatureLen {
|
|
return "", fmt.Errorf("signature exceeds maximum length (%d bytes)", maxBypassSignatureLen)
|
|
}
|
|
|
|
switch sig[0] {
|
|
case 'R':
|
|
if err := validateDoubleLayerSignature(sig); err != nil {
|
|
return "", err
|
|
}
|
|
return sig, nil
|
|
case 'E':
|
|
if err := validateSingleLayerSignature(sig); err != nil {
|
|
return "", err
|
|
}
|
|
return base64.StdEncoding.EncodeToString([]byte(sig)), nil
|
|
default:
|
|
return "", fmt.Errorf("invalid signature: expected 'E' or 'R' prefix, got %q", string(sig[0]))
|
|
}
|
|
}
|
|
|
|
func validateDoubleLayerSignature(sig string) error {
|
|
decoded, err := base64.StdEncoding.DecodeString(sig)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid double-layer signature: base64 decode failed: %w", err)
|
|
}
|
|
if len(decoded) == 0 {
|
|
return fmt.Errorf("invalid double-layer signature: empty after decode")
|
|
}
|
|
if decoded[0] != 'E' {
|
|
return fmt.Errorf("invalid double-layer signature: inner does not start with 'E', got 0x%02x", decoded[0])
|
|
}
|
|
return validateSingleLayerSignatureContent(string(decoded), 2)
|
|
}
|
|
|
|
func validateSingleLayerSignature(sig string) error {
|
|
return validateSingleLayerSignatureContent(sig, 1)
|
|
}
|
|
|
|
func validateSingleLayerSignatureContent(sig string, encodingLayers int) error {
|
|
decoded, err := base64.StdEncoding.DecodeString(sig)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid single-layer signature: base64 decode failed: %w", err)
|
|
}
|
|
if len(decoded) == 0 {
|
|
return fmt.Errorf("invalid single-layer signature: empty after decode")
|
|
}
|
|
if decoded[0] != 0x12 {
|
|
return fmt.Errorf("invalid Claude signature: expected first byte 0x12, got 0x%02x", decoded[0])
|
|
}
|
|
if !cache.SignatureBypassStrictMode() {
|
|
return nil
|
|
}
|
|
_, err = inspectClaudeSignaturePayload(decoded, encodingLayers)
|
|
return err
|
|
}
|
|
|
|
func inspectDoubleLayerSignature(sig string) (*claudeSignatureTree, error) {
|
|
decoded, err := base64.StdEncoding.DecodeString(sig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid double-layer signature: base64 decode failed: %w", err)
|
|
}
|
|
if len(decoded) == 0 {
|
|
return nil, fmt.Errorf("invalid double-layer signature: empty after decode")
|
|
}
|
|
if decoded[0] != 'E' {
|
|
return nil, fmt.Errorf("invalid double-layer signature: inner does not start with 'E', got 0x%02x", decoded[0])
|
|
}
|
|
return inspectSingleLayerSignatureWithLayers(string(decoded), 2)
|
|
}
|
|
|
|
func inspectSingleLayerSignature(sig string) (*claudeSignatureTree, error) {
|
|
return inspectSingleLayerSignatureWithLayers(sig, 1)
|
|
}
|
|
|
|
func inspectSingleLayerSignatureWithLayers(sig string, encodingLayers int) (*claudeSignatureTree, error) {
|
|
decoded, err := base64.StdEncoding.DecodeString(sig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid single-layer signature: base64 decode failed: %w", err)
|
|
}
|
|
if len(decoded) == 0 {
|
|
return nil, fmt.Errorf("invalid single-layer signature: empty after decode")
|
|
}
|
|
return inspectClaudeSignaturePayload(decoded, encodingLayers)
|
|
}
|
|
|
|
func inspectClaudeSignaturePayload(payload []byte, encodingLayers int) (*claudeSignatureTree, error) {
|
|
if len(payload) == 0 {
|
|
return nil, fmt.Errorf("invalid Claude signature: empty payload")
|
|
}
|
|
if payload[0] != 0x12 {
|
|
return nil, fmt.Errorf("invalid Claude signature: expected first byte 0x12, got 0x%02x", payload[0])
|
|
}
|
|
container, err := extractBytesField(payload, 2, "top-level protobuf")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channelBlock, err := extractBytesField(container, 1, "Claude Field 2 container")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return inspectClaudeChannelBlock(channelBlock, encodingLayers)
|
|
}
|
|
|
|
func inspectClaudeChannelBlock(channelBlock []byte, encodingLayers int) (*claudeSignatureTree, error) {
|
|
tree := &claudeSignatureTree{
|
|
EncodingLayers: encodingLayers,
|
|
RoutingClass: "unknown",
|
|
InfrastructureClass: "infra_unknown",
|
|
SchemaFeatures: "unknown_schema_features",
|
|
}
|
|
haveChannelID := false
|
|
hasField6 := false
|
|
hasField7 := false
|
|
|
|
err := walkProtobufFields(channelBlock, func(num protowire.Number, typ protowire.Type, raw []byte) error {
|
|
switch num {
|
|
case 1:
|
|
if typ != protowire.VarintType {
|
|
return fmt.Errorf("invalid Claude signature: Field 2.1.1 channel_id must be varint")
|
|
}
|
|
channelID, err := decodeVarintField(raw, "Field 2.1.1 channel_id")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tree.ChannelID = channelID
|
|
haveChannelID = true
|
|
case 2:
|
|
if typ != protowire.VarintType {
|
|
return fmt.Errorf("invalid Claude signature: Field 2.1.2 field2 must be varint")
|
|
}
|
|
field2, err := decodeVarintField(raw, "Field 2.1.2 field2")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tree.Field2 = &field2
|
|
case 6:
|
|
if typ != protowire.BytesType {
|
|
return fmt.Errorf("invalid Claude signature: Field 2.1.6 model_text must be bytes")
|
|
}
|
|
modelBytes, err := decodeBytesField(raw, "Field 2.1.6 model_text")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !utf8.Valid(modelBytes) {
|
|
return fmt.Errorf("invalid Claude signature: Field 2.1.6 model_text is not valid UTF-8")
|
|
}
|
|
tree.ModelText = string(modelBytes)
|
|
hasField6 = true
|
|
case 7:
|
|
if typ != protowire.VarintType {
|
|
return fmt.Errorf("invalid Claude signature: Field 2.1.7 must be varint")
|
|
}
|
|
if _, err := decodeVarintField(raw, "Field 2.1.7"); err != nil {
|
|
return err
|
|
}
|
|
hasField7 = true
|
|
tree.HasField7 = true
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !haveChannelID {
|
|
return nil, fmt.Errorf("invalid Claude signature: missing Field 2.1.1 channel_id")
|
|
}
|
|
|
|
switch tree.ChannelID {
|
|
case 11:
|
|
tree.RoutingClass = "routing_class_11"
|
|
case 12:
|
|
tree.RoutingClass = "routing_class_12"
|
|
}
|
|
|
|
if tree.Field2 == nil {
|
|
tree.InfrastructureClass = "infra_default"
|
|
} else {
|
|
switch *tree.Field2 {
|
|
case 1:
|
|
tree.InfrastructureClass = "infra_aws"
|
|
case 2:
|
|
tree.InfrastructureClass = "infra_google"
|
|
default:
|
|
tree.InfrastructureClass = "infra_unknown"
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case hasField6:
|
|
tree.SchemaFeatures = "extended_model_tagged_schema"
|
|
case !hasField6 && !hasField7 && len(channelBlock) >= 70 && len(channelBlock) <= 72:
|
|
tree.SchemaFeatures = "compact_schema"
|
|
}
|
|
|
|
if tree.ChannelID == 11 {
|
|
switch {
|
|
case tree.Field2 == nil:
|
|
tree.LegacyRouteHint = "legacy_default_group"
|
|
case *tree.Field2 == 1:
|
|
tree.LegacyRouteHint = "legacy_aws_group"
|
|
case *tree.Field2 == 2 && tree.EncodingLayers == 2:
|
|
tree.LegacyRouteHint = "legacy_vertex_direct"
|
|
case *tree.Field2 == 2 && tree.EncodingLayers == 1:
|
|
tree.LegacyRouteHint = "legacy_vertex_proxy"
|
|
}
|
|
}
|
|
|
|
return tree, nil
|
|
}
|
|
|
|
func extractBytesField(msg []byte, fieldNum protowire.Number, scope string) ([]byte, error) {
|
|
var value []byte
|
|
err := walkProtobufFields(msg, func(num protowire.Number, typ protowire.Type, raw []byte) error {
|
|
if num != fieldNum {
|
|
return nil
|
|
}
|
|
if typ != protowire.BytesType {
|
|
return fmt.Errorf("invalid Claude signature: %s field %d must be bytes", scope, fieldNum)
|
|
}
|
|
bytesValue, err := decodeBytesField(raw, fmt.Sprintf("%s field %d", scope, fieldNum))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value = bytesValue
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if value == nil {
|
|
return nil, fmt.Errorf("invalid Claude signature: missing %s field %d", scope, fieldNum)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func walkProtobufFields(msg []byte, visit func(num protowire.Number, typ protowire.Type, raw []byte) error) error {
|
|
for offset := 0; offset < len(msg); {
|
|
num, typ, n := protowire.ConsumeTag(msg[offset:])
|
|
if n < 0 {
|
|
return fmt.Errorf("invalid Claude signature: malformed protobuf tag: %w", protowire.ParseError(n))
|
|
}
|
|
offset += n
|
|
valueLen := protowire.ConsumeFieldValue(num, typ, msg[offset:])
|
|
if valueLen < 0 {
|
|
return fmt.Errorf("invalid Claude signature: malformed protobuf field %d: %w", num, protowire.ParseError(valueLen))
|
|
}
|
|
fieldRaw := msg[offset : offset+valueLen]
|
|
if err := visit(num, typ, fieldRaw); err != nil {
|
|
return err
|
|
}
|
|
offset += valueLen
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func decodeVarintField(raw []byte, label string) (uint64, error) {
|
|
value, n := protowire.ConsumeVarint(raw)
|
|
if n < 0 {
|
|
return 0, fmt.Errorf("invalid Claude signature: failed to decode %s: %w", label, protowire.ParseError(n))
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func decodeBytesField(raw []byte, label string) ([]byte, error) {
|
|
value, n := protowire.ConsumeBytes(raw)
|
|
if n < 0 {
|
|
return nil, fmt.Errorf("invalid Claude signature: failed to decode %s: %w", label, protowire.ParseError(n))
|
|
}
|
|
return value, nil
|
|
}
|