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 行为完全保留:无 '#' 前缀的原生签名静默丢弃
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user