vibe-proxy/backend/internal/signature/grok_validation_test.go
2026-08-24 00:10:41 +02:00

392 lines
14 KiB
Go

package signature
import (
"bytes"
"encoding/base64"
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"google.golang.org/protobuf/encoding/protowire"
)
func TestInspectGrokEncryptedContent_NativeSamples(t *testing.T) {
path, ok := grokEncryptedContentSamplesPath()
if !ok {
t.Skip("grok encrypted_content corpus missing; run docs/native-prompt-capture/scripts/harvest-grok-encrypted-content.sh")
}
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read samples: %v", err)
}
var samples []string
if err := json.Unmarshal(raw, &samples); err != nil {
t.Fatalf("unmarshal samples: %v", err)
}
if len(samples) == 0 {
t.Fatal("expected native Grok encrypted_content samples")
}
for i, sample := range samples {
if _, err := InspectGrokEncryptedContent(sample); err != nil {
t.Fatalf("sample[%d] should be valid, got %v", i, err)
}
}
}
func TestInspectGrokEncryptedContent_RejectsAgyGeminiThoughtSignatures(t *testing.T) {
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
path := filepath.Join(filepath.Dir(file), "testdata", "agy_gemini_thought_signature_entries.json")
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Skip("agy gemini corpus missing; run harvest_agy_gemini_signatures.py")
} else if err != nil {
t.Fatalf("stat samples: %v", err)
}
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read samples: %v", err)
}
var entries []struct {
ThoughtSignature string `json:"thoughtSignature"`
}
if err := json.Unmarshal(raw, &entries); err != nil {
t.Fatalf("unmarshal samples: %v", err)
}
if len(entries) == 0 {
t.Fatal("expected agy Gemini thought signatures")
}
checkedUnpaddedGemini := false
for i, entry := range entries {
_, err := InspectGrokEncryptedContent(entry.ThoughtSignature)
if err == nil {
t.Fatalf("entry[%d] should not pass as Grok encrypted_content", i)
}
if !strings.Contains(entry.ThoughtSignature, "=") {
checkedUnpaddedGemini = true
if !strings.Contains(err.Error(), "Gemini") {
t.Fatalf("entry[%d] error = %q, want Gemini fast-reject detail", i, err.Error())
}
}
}
if !checkedUnpaddedGemini {
t.Fatal("expected at least one unpadded Gemini thought signature sample")
}
}
func TestInspectGrokEncryptedContent_RejectsGeminiThoughtSignatureEnvelope(t *testing.T) {
sample := testGeminiThoughtSignatureEnvelope()
_, err := InspectGrokEncryptedContent(sample)
if err == nil {
t.Fatal("expected Gemini thoughtSignature envelope to be rejected")
}
if !strings.Contains(err.Error(), "Gemini") {
t.Fatalf("error = %q, want Gemini fast-reject detail", err.Error())
}
}
// TestInspectGrokEncryptedContent_RetiredGemini25Field1Envelope covers the
// retired Gemini 2.5 envelope. It is no longer a known Gemini envelope, so the
// Gemini fast-reject no longer fires for it and it falls to the residual class
// like any other opaque payload. Recorded here so the change is deliberate rather
// than an accident of the Gemini validator being narrowed.
func TestInspectGrokEncryptedContent_RetiredGemini25Field1Envelope(t *testing.T) {
sample := testGemini25Field1ThoughtSignatureEnvelope()
if IsValidGeminiThoughtSignature(sample, GeminiThoughtSignatureValidationOptions{RequireKnownEnvelope: true}) {
t.Fatal("fixture should no longer be a known Gemini thoughtSignature")
}
if _, err := InspectGrokEncryptedContent(sample); err != nil {
t.Fatalf("retired envelope should reach the residual transport check, got %v", err)
}
}
func TestInspectGrokEncryptedContent_RejectsClaudeThinkingSignature(t *testing.T) {
sample := testUnpaddedClaudeThinkingSignature()
if !IsValidClaudeThinkingSignature(sample, ClaudeSignatureValidationOptions{Strict: true}) {
t.Fatal("fixture should be a strict Claude thinking signature")
}
_, err := InspectGrokEncryptedContent(sample)
if err == nil {
t.Fatal("expected Claude thinking signature to be rejected")
}
if !strings.Contains(err.Error(), "Claude") {
t.Fatalf("error = %q, want Claude fast-reject detail", err.Error())
}
}
func TestInspectGrokEncryptedContent_RejectsAntigravityClaudeThinkingSignature(t *testing.T) {
sample := testUnpaddedAntigravityClaudeThinkingSignature()
if !strings.HasPrefix(sample, "R") || strings.Contains(sample, "=") {
t.Fatalf("fixture should be an unpadded R-form Claude signature, got prefix=%q has_padding=%t", sample[:1], strings.Contains(sample, "="))
}
if !IsValidClaudeThinkingSignature(sample, ClaudeSignatureValidationOptions{Strict: true}) {
t.Fatal("fixture should be a strict Antigravity Claude thinking signature")
}
_, err := InspectGrokEncryptedContent(sample)
if err == nil {
t.Fatal("expected Antigravity Claude thinking signature to be rejected")
}
if !strings.Contains(err.Error(), "Claude") {
t.Fatalf("error = %q, want Claude fast-reject detail", err.Error())
}
}
// TestInspectGrokEncryptedContent_RejectsClaudeCAISSignature covers the CAIS
// envelope emitted by the newest Claude Code models. CAIS payloads are
// high-entropy standard base64 and drop their padding whenever the decoded
// length is a multiple of 3, so neither the padding gate nor the classic Claude
// strict check excludes them on their own.
func TestInspectGrokEncryptedContent_RejectsClaudeCAISSignature(t *testing.T) {
cases := []struct {
name string
sample string
}{
{name: "synthetic unpadded", sample: testUnpaddedClaudeCAISSignature()},
{name: "observed fable-5", sample: observedFable5Sample},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if strings.Contains(tc.sample, "=") {
t.Fatal("fixture must be unpadded so it reaches the Claude CAIS check")
}
if !IsValidClaudeCAISSignature(tc.sample) {
t.Fatal("fixture should be a valid Claude CAIS signature")
}
if IsValidClaudeThinkingSignature(tc.sample, ClaudeSignatureValidationOptions{Strict: true}) {
t.Fatal("CAIS fixture must not also pass classic Claude validation")
}
_, err := InspectGrokEncryptedContent(tc.sample)
if err == nil {
t.Fatal("expected Claude CAIS signature to be rejected")
}
if !strings.Contains(err.Error(), "CAIS") {
t.Fatalf("error = %q, want Claude CAIS fast-reject detail", err.Error())
}
})
}
}
// TestInspectGrokEncryptedContent_RejectsProviderCachePrefix keeps provenance
// envelopes out of the residual class. A prefixed value belongs to whichever
// provider the prefix names, and must never be replayed to xAI verbatim.
func TestInspectGrokEncryptedContent_RejectsProviderCachePrefix(t *testing.T) {
for _, prefix := range []string{"claude#", "anthropic#", "gemini#", "openai#", "codex#"} {
sample := prefix + testUnpaddedClaudeCAISSignature()
if _, err := InspectGrokEncryptedContent(sample); err == nil {
t.Fatalf("%s prefixed payload should be rejected", prefix)
}
}
}
// TestInspectGrokEncryptedContent_ThresholdMargins documents that neither
// threshold sits on observed data. The shortest observed native payload is 50
// decoded bytes and the lowest observed entropy ratio is 0.892, so both limits
// keep headroom for future models rather than fitting the current corpus exactly.
func TestInspectGrokEncryptedContent_ThresholdMargins(t *testing.T) {
const shortestObservedDecodedLen = 50
const lowestObservedEntropyRatio = 0.892
if MinGrokEncryptedContentDecodedLen >= shortestObservedDecodedLen {
t.Fatalf("MinGrokEncryptedContentDecodedLen = %d, want below the shortest observed payload (%d) so a shorter future payload is not silently dropped",
MinGrokEncryptedContentDecodedLen, shortestObservedDecodedLen)
}
if MinGrokEncryptedContentEntropyRatio >= lowestObservedEntropyRatio {
t.Fatalf("MinGrokEncryptedContentEntropyRatio = %.3f, want below the lowest observed ratio (%.3f)",
MinGrokEncryptedContentEntropyRatio, lowestObservedEntropyRatio)
}
}
func TestInspectGrokEncryptedContent_RejectsForeignShapes(t *testing.T) {
cases := []string{
"",
"bad",
" opaque",
"gAAAAABinvalid-gpt-shape",
"abcd_efg",
base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0xa5}, MinGrokEncryptedContentDecodedLen)),
}
for _, sample := range cases {
if _, err := InspectGrokEncryptedContent(sample); err == nil {
t.Fatalf("expected invalid Grok encrypted_content, got pass for %q", sample)
}
}
}
func TestInspectGrokEncryptedContent_RejectsLowEntropyPayload(t *testing.T) {
sample := base64.RawStdEncoding.EncodeToString(bytes.Repeat([]byte{0xa5}, MinGrokEncryptedContentDecodedLen))
_, err := InspectGrokEncryptedContent(sample)
if err == nil {
t.Fatal("expected low-entropy payload to be rejected")
}
if !strings.Contains(err.Error(), "entropy ratio") {
t.Fatalf("error = %q, want entropy ratio detail", err.Error())
}
}
func TestInspectGrokEncryptedContent_RejectsInvalidBase64Length(t *testing.T) {
_, err := InspectGrokEncryptedContent("AAAAA")
if err == nil {
t.Fatal("expected invalid base64 length to be rejected")
}
if !strings.Contains(err.Error(), "base64 decode failed") {
t.Fatalf("error = %q, want base64 decode detail", err.Error())
}
}
func TestByteEntropyRatio_SingleByteReturnsZero(t *testing.T) {
if got := byteEntropyRatio([]byte{0xa5}); got != 0 {
t.Fatalf("byteEntropyRatio(single byte) = %v, want 0", got)
}
}
func testGeminiThoughtSignatureEnvelope() string {
payload := []byte{0x01, 0x0c}
for i := 0; i < 97; i++ {
payload = append(payload, byte(i))
}
inner := []byte{0x0a, byte(len(payload))}
inner = append(inner, payload...)
outer := []byte{0x12, byte(len(inner))}
outer = append(outer, inner...)
return base64.RawStdEncoding.EncodeToString(outer)
}
func testGemini25Field1ThoughtSignatureEnvelope() string {
payload := []byte{0x01}
for i := 0; len(payload) < 128; i++ {
payload = append(payload, byte((i*37+11)%251))
}
var decoded []byte
decoded = protowire.AppendTag(decoded, 1, protowire.BytesType)
decoded = protowire.AppendBytes(decoded, payload)
return base64.RawStdEncoding.EncodeToString(decoded)
}
func testUnpaddedClaudeThinkingSignature() string {
return testClaudeThinkingSignatureWithOpaqueLen(35)
}
// testUnpaddedClaudeCAISSignature builds a CAIS signature whose base64 form
// carries no "=" padding, which is the shape that used to slip past the Grok
// unpadded-base64 gate. The model text length is varied because padding depends
// on the encoded payload length.
func testUnpaddedClaudeCAISSignature() string {
for suffix := 0; suffix < 8; suffix++ {
parts := defaultClaudeCAISParts("claude-opus-5" + strings.Repeat("x", suffix))
if sample := parts.encode(); !strings.Contains(sample, "=") {
return sample
}
}
panic("could not build an unpadded Claude CAIS fixture")
}
func testUnpaddedAntigravityClaudeThinkingSignature() string {
return base64.StdEncoding.EncodeToString([]byte(testClaudeThinkingSignatureWithOpaqueLen(41)))
}
func testClaudeThinkingSignatureWithOpaqueLen(opaqueLen int) string {
var channelBlock []byte
channelBlock = protowire.AppendTag(channelBlock, 1, protowire.VarintType)
channelBlock = protowire.AppendVarint(channelBlock, 12)
channelBlock = protowire.AppendTag(channelBlock, 2, protowire.VarintType)
channelBlock = protowire.AppendVarint(channelBlock, 2)
channelBlock = protowire.AppendTag(channelBlock, 6, protowire.BytesType)
channelBlock = protowire.AppendString(channelBlock, "claude-sonnet-4-6")
var container []byte
container = protowire.AppendTag(container, 1, protowire.BytesType)
container = protowire.AppendBytes(container, channelBlock)
var payload []byte
payload = protowire.AppendTag(payload, 2, protowire.BytesType)
payload = protowire.AppendBytes(payload, container)
payload = protowire.AppendTag(payload, 3, protowire.VarintType)
payload = protowire.AppendVarint(payload, 1)
payload = protowire.AppendTag(payload, 4, protowire.BytesType)
opaque := make([]byte, 0, opaqueLen)
for i := 0; len(opaque) < opaqueLen; i++ {
opaque = append(opaque, byte((i*41+17)%251))
}
payload = protowire.AppendBytes(payload, opaque)
return base64.StdEncoding.EncodeToString(payload)
}
func grokEncryptedContentSamplesPath() (string, bool) {
_, file, _, ok := runtime.Caller(0)
if !ok {
return "", false
}
repo := filepath.Clean(filepath.Join(filepath.Dir(file), "..", ".."))
path := filepath.Join(repo, "docs", "native-prompt-capture", "corpus", "grok-encrypted-content", "samples.json")
if _, err := os.Stat(path); err != nil {
return path, false
}
return path, true
}
func TestSignatureProviderFromModelName_Grok(t *testing.T) {
for _, model := range []string{"grok-4.5", "grok-4.5-build", "grok-composer-2.5-fast", "grok-code-fast-1"} {
t.Run(model, func(t *testing.T) {
if got := SignatureProviderFromModelName(model); got != SignatureProviderGrok {
t.Errorf("SignatureProviderFromModelName(%q) = %q, want %q", model, got, SignatureProviderGrok)
}
})
}
}
// TestDetectSignatureProvider_NeverClassifiesGrok pins the contract that xAI is
// a target-only family. Its ciphertext carries no envelope, no version byte and
// no fixed length, so a positive detection rule would necessarily also claim
// unrelated opaque payloads. Callers establish an xAI target from provenance and
// then use InspectGrokEncryptedContent as a replay-safety check.
func TestDetectSignatureProvider_NeverClassifiesGrok(t *testing.T) {
path, ok := grokEncryptedContentSamplesPath()
if !ok {
t.Skip("grok encrypted_content corpus missing; run docs/native-prompt-capture/scripts/harvest-grok-encrypted-content.sh")
}
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read grok corpus: %v", err)
}
var samples []string
if err := json.Unmarshal(raw, &samples); err != nil {
var wrapped struct {
Samples []string `json:"samples"`
}
if err := json.Unmarshal(raw, &wrapped); err != nil {
t.Fatalf("parse grok corpus: %v", err)
}
samples = wrapped.Samples
}
if len(samples) == 0 {
t.Skip("grok encrypted_content corpus is empty")
}
for _, sig := range samples {
if got := DetectSignatureProvider(sig); got != SignatureProviderUnknown {
t.Fatalf("DetectSignatureProvider = %q, want %q for native encrypted_content", got, SignatureProviderUnknown)
}
}
}
// TestDecideSignatureCompatibility_GrokDropsBlock contrasts with the Kimi
// policy: xAI decrypts the blob and answers 400 for foreign or mutated input, so
// an incompatible block cannot survive by shedding just its signature.
func TestDecideSignatureCompatibility_GrokDropsBlock(t *testing.T) {
decision := DecideSignatureCompatibility(SignatureProviderGrok, observedFable5Sample, SignatureBlockKindUnknown)
if decision.Compatible {
t.Fatalf("Claude signature reported compatible with a Grok target")
}
if decision.Action != SignatureActionDropBlock {
t.Errorf("Action = %q, want %q", decision.Action, SignatureActionDropBlock)
}
}