Files
CLIProxyAPI/internal/translator/antigravity/claude/signature_validation.go
T
sususu98 cf249586a9 feat(antigravity): configurable signature cache with bypass-mode validation
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 行为完全保留:无 '#' 前缀的原生签名静默丢弃
2026-04-09 21:12:40 +08:00

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
}