Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
161
backend/internal/signature/kimi_validation.go
Normal file
161
backend/internal/signature/kimi_validation.go
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
package signature
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Kimi thinking signatures carry no self-describing envelope. Every byte is
|
||||
// indistinguishable from uniform random data: a per-offset scan over 44 samples
|
||||
// x 9709 bytes found zero positions below a 4-sigma floor, so there is no magic
|
||||
// prefix, version byte, key id or timestamp to anchor on the way GPT (Fernet),
|
||||
// Claude (CAIS/protobuf) and Gemini (Tink envelope) all provide.
|
||||
//
|
||||
// What Kimi does expose is size. The raw signature length is fixed per protocol
|
||||
// mode and completely independent of the content it accompanies:
|
||||
//
|
||||
// non-streaming : 12946 characters (9709 bytes)
|
||||
// streaming : 4340 characters (3255 bytes)
|
||||
//
|
||||
// This is not quantization of a variable payload into buckets. There is no
|
||||
// bucketing behaviour at all: a response whose thinking text grew from 6 to
|
||||
// 14,803 characters (thinking_tokens 1 -> 6188, output_tokens 29 -> 2709) emits
|
||||
// a byte-identical signature length, and a non-streaming response carrying a
|
||||
// single thinking token still emits the full 12946. The two values are code-path
|
||||
// constants, not size classes.
|
||||
//
|
||||
// Empirical basis for treating the pair as complete:
|
||||
// - All 8 Kimi models exposed upstream (k2, k2.5, k2.6, k2-thinking, k2.7-code,
|
||||
// k2.7-code-highspeed, k3, k3-256k) x streaming/non-streaming = 16 combinations,
|
||||
// no exceptions.
|
||||
// - Additional paths that produced no third value: thinking budget 128..12000,
|
||||
// absent thinking field, max_tokens truncation mid-thinking, non-English
|
||||
// prompts, 135k-character inputs, multi-turn replay of signed history, tool
|
||||
// calls, tool_result continuation, and the interleaved-thinking beta header.
|
||||
// - Two independent collection paths agree: CPA request logs (57 unique samples)
|
||||
// and the mitmproxy harvest in
|
||||
// .agents/skills/cpa-signature-catalog-and-collection/data/signatures/kimi/
|
||||
// (61 unique samples) both yield exactly {4340, 12946}.
|
||||
//
|
||||
// Cross-family safety: across 1027 catalog signatures plus 215 native Grok
|
||||
// samples, no Claude, Gemini, GPT or Grok value lands on either length. The
|
||||
// nearest miss is a 4344-character GPT token, which the gAAAA probe claims long
|
||||
// before this check runs.
|
||||
//
|
||||
// Fragility this check accepts, and why it still runs last: the length pair is
|
||||
// an observed regularity, not a protocol contract. Kimi never reads the field
|
||||
// back - replaying an empty string, a single character, non-base64 text or a
|
||||
// mutated blob all return 200, and omitting the signature entirely also returns
|
||||
// 200, because reasoning continuity on that endpoint travels in OpenAI-style
|
||||
// reasoning_content instead. A gateway change could therefore move these values
|
||||
// without any client-visible error. Running the self-describing validators first
|
||||
// bounds the damage: a drift only costs Kimi its own identification and cannot
|
||||
// mislabel another provider's signature.
|
||||
const (
|
||||
// KimiThinkingSignatureNonStreamingLen is the raw character length Kimi emits
|
||||
// for non-streaming Messages responses.
|
||||
KimiThinkingSignatureNonStreamingLen = 12946
|
||||
// KimiThinkingSignatureStreamingLen is the raw character length Kimi emits in
|
||||
// the streaming signature_delta event.
|
||||
KimiThinkingSignatureStreamingLen = 4340
|
||||
)
|
||||
|
||||
// KimiThinkingSignatureMode records which upstream code path produced a
|
||||
// signature. It is derived from length alone and carries no decoded content.
|
||||
type KimiThinkingSignatureMode string
|
||||
|
||||
const (
|
||||
KimiThinkingSignatureModeNonStreaming KimiThinkingSignatureMode = "non_streaming"
|
||||
KimiThinkingSignatureModeStreaming KimiThinkingSignatureMode = "streaming"
|
||||
)
|
||||
|
||||
// kimiThinkingSignatureLens maps every accepted raw length to the mode that
|
||||
// produces it. Keeping this as a package-level map rather than inline constants
|
||||
// leaves room for a calibration pass to register a newly observed length without
|
||||
// touching the probe itself.
|
||||
var kimiThinkingSignatureLens = map[int]KimiThinkingSignatureMode{
|
||||
KimiThinkingSignatureNonStreamingLen: KimiThinkingSignatureModeNonStreaming,
|
||||
KimiThinkingSignatureStreamingLen: KimiThinkingSignatureModeStreaming,
|
||||
}
|
||||
|
||||
// MinKimiThinkingSignatureEntropyRatio keeps a same-length attacker-supplied
|
||||
// filler from claiming the family. Native samples sit at 0.997+ against the
|
||||
// sample-size ceiling, so this floor has multiple sigma of headroom while still
|
||||
// rejecting padded or repetitive input.
|
||||
const MinKimiThinkingSignatureEntropyRatio = 0.85
|
||||
|
||||
// KimiThinkingSignatureInfo describes an accepted Kimi thinking signature.
|
||||
type KimiThinkingSignatureInfo struct {
|
||||
RawLen int
|
||||
DecodedLen int
|
||||
Mode KimiThinkingSignatureMode
|
||||
}
|
||||
|
||||
// InspectKimiThinkingSignature validates the transport shape of a Kimi Messages
|
||||
// thinking signature.
|
||||
//
|
||||
// Unlike the Claude, Gemini and GPT validators this proves nothing about the
|
||||
// payload: it reports that the value has the size and character class Kimi
|
||||
// produces. Because size is the only available signal, this probe must run after
|
||||
// every self-describing envelope check has declined, so that a Claude, Gemini or
|
||||
// GPT signature can never be captured by a length coincidence.
|
||||
func InspectKimiThinkingSignature(raw string) (*KimiThinkingSignatureInfo, error) {
|
||||
sig := strings.TrimSpace(raw)
|
||||
if sig == "" {
|
||||
return nil, fmt.Errorf("empty Kimi thinking signature")
|
||||
}
|
||||
if sig != raw {
|
||||
return nil, fmt.Errorf("Kimi thinking signature has leading or trailing whitespace")
|
||||
}
|
||||
mode, ok := kimiThinkingSignatureLens[len(sig)]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid Kimi thinking signature: unexpected length %d", len(sig))
|
||||
}
|
||||
if strings.Contains(sig, "=") {
|
||||
return nil, fmt.Errorf("invalid Kimi thinking signature: expected unpadded standard base64")
|
||||
}
|
||||
if index, r, ok := firstInvalidGrokEncryptedContentChar(sig); ok {
|
||||
return nil, fmt.Errorf("invalid Kimi thinking signature: contains non-base64 character U+%04X at byte %d", r, index)
|
||||
}
|
||||
if _, _, ok := SplitSignatureProviderPrefix(sig); ok {
|
||||
return nil, fmt.Errorf("invalid Kimi thinking signature: carries another provider's cache prefix")
|
||||
}
|
||||
// Defense in depth. DetectSignatureProviderForBlock already runs the
|
||||
// self-describing probes first, but this validator is exported and callers
|
||||
// may reach it directly, so a foreign envelope of coincidentally matching
|
||||
// length must not be accepted here either.
|
||||
if maybeSelfDescribingSignatureEnvelope(sig) {
|
||||
if strings.HasPrefix(sig, "gAAAA") {
|
||||
return nil, fmt.Errorf("Kimi thinking signature looks like GPT/Codex reasoning signature")
|
||||
}
|
||||
if IsValidClaudeCAISSignature(sig) {
|
||||
return nil, fmt.Errorf("Kimi thinking signature looks like Claude CAIS thinking signature")
|
||||
}
|
||||
if IsValidClaudeThinkingSignature(sig, ClaudeSignatureValidationOptions{Strict: true}) {
|
||||
return nil, fmt.Errorf("Kimi thinking signature looks like Claude thinking signature")
|
||||
}
|
||||
if IsValidGeminiThoughtSignature(sig, GeminiThoughtSignatureValidationOptions{RequireKnownEnvelope: true}) {
|
||||
return nil, fmt.Errorf("Kimi thinking signature looks like Gemini thoughtSignature")
|
||||
}
|
||||
}
|
||||
decoded, err := base64.RawStdEncoding.DecodeString(sig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid Kimi thinking signature: base64 decode failed: %w", err)
|
||||
}
|
||||
if entropyRatio := byteEntropyRatio(decoded); entropyRatio < MinKimiThinkingSignatureEntropyRatio {
|
||||
return nil, fmt.Errorf("invalid Kimi thinking signature: decoded payload entropy ratio %.3f below %.3f", entropyRatio, MinKimiThinkingSignatureEntropyRatio)
|
||||
}
|
||||
return &KimiThinkingSignatureInfo{
|
||||
RawLen: len(sig),
|
||||
DecodedLen: len(decoded),
|
||||
Mode: mode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsValidKimiThinkingSignature reports whether raw has the transport shape of a
|
||||
// Kimi thinking signature.
|
||||
func IsValidKimiThinkingSignature(raw string) bool {
|
||||
_, err := InspectKimiThinkingSignature(raw)
|
||||
return err == nil
|
||||
}
|
||||
Loading…
Reference in a new issue