Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
392
backend/internal/signature/grok_validation_test.go
Normal file
392
backend/internal/signature/grok_validation_test.go
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue