cf249586a9
Antigravity 的 Claude thinking signature 处理新增 cache/bypass 双模式, 并为 bypass 模式实现按 SIGNATURE-CHANNEL-SPEC.md 的签名校验。 新增 antigravity-signature-cache-enabled 配置项(默认 true): - cache mode(true):使用服务端缓存的签名,行为与原有逻辑完全一致 - bypass mode(false):直接使用客户端提供的签名,经过校验和归一化 支持配置热重载,运行时可切换模式。 校验流程: 1. 剥离历史 cache-mode 的 'modelGroup#' 前缀(如 claude#Exxxx → Exxxx) 2. 首字符必须为 'E'(单层编码)或 'R'(双层编码),否则拒绝 3. R 开头:base64 解码 → 内层必须以 'E' 开头 → 继续单层校验 4. E 开头:base64 解码 → 首字节必须为 0x12(Claude protobuf 标识) 5. 所有合法签名归一化为 R 形式(双层 base64)发往 Antigravity 后端 非法签名处理策略: - 非严格模式(默认):translator 静默丢弃无签名的 thinking block - 严格模式(antigravity-signature-bypass-strict: true): executor 层在请求发往上游前直接返回 HTTP 400 按 SIGNATURE-CHANNEL-SPEC.md 解析 Claude 签名的完整 protobuf 结构: - Top-level Field 2(容器)→ Field 1(渠道块) - 渠道块提取:channel_id (Field 1)、infrastructure (Field 2)、 model_text (Field 6)、field7 (Field 7) - 计算 routing_class、infrastructure_class、schema_features - 使用 google.golang.org/protobuf/encoding/protowire 解析 - resolveThinkingSignature 拆分为 resolveCacheModeSignature / resolveBypassModeSignature - hasResolvedThinkingSignature:mode-aware 签名有效性判断 (cache: len>=50 via HasValidSignature,bypass: non-empty) - validateAntigravityRequestSignatures:executor 预检, 仅在 bypass + strict 模式下拦截非法签名返回 400 - 响应侧签名缓存逻辑与 cache mode 集成 - Cache mode 行为完全保留:无 '#' 前缀的原生签名静默丢弃
352 lines
10 KiB
Go
352 lines
10 KiB
Go
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"
|
|
)
|
|
|
|
// maxBypassSignatureLen caps the signature string length (after prefix stripping)
|
|
// to prevent base64 decode from allocating excessive memory on malicious input.
|
|
const maxBypassSignatureLen = 8192
|
|
|
|
type claudeSignatureTree struct {
|
|
EncodingLayers int
|
|
ChannelID uint64
|
|
Field2 *uint64
|
|
RoutingClass string
|
|
InfrastructureClass string
|
|
SchemaFeatures string
|
|
ModelText string
|
|
LegacyRouteHint string
|
|
HasField7 bool
|
|
}
|
|
|
|
// ValidateClaudeBypassSignatures validates Claude thinking signatures in bypass mode.
|
|
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
|
|
}
|
|
|
|
// normalizeClaudeBypassSignature validates a raw Claude signature and returns
|
|
// it in the double-layer (R-starting) form expected by upstream.
|
|
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"
|
|
case *tree.Field2 == 2:
|
|
tree.LegacyRouteHint = "legacy_vertex_group"
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|