// integrity_test.go - SHA-256 plugin 完整性校验测试. package plugin import ( "errors" "os" "path/filepath" "strings" "testing" ) // makeTestPluginDir 创建一个最小合法 plugin 目录 (含 plugin.json), 可选额外文件. // 返回目录绝对路径. 使用 t.TempDir 自动清理. func makeTestPluginDir(t *testing.T, extra map[string]string) string { t.Helper() dir := t.TempDir() manifest := `{"name":"test-plugin","version":"0.1.0","description":"test"}` if err := os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(manifest), 0o644); err != nil { t.Fatal(err) } for rel, content := range extra { full := filepath.Join(dir, filepath.FromSlash(rel)) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(full, []byte(content), 0o644); err != nil { t.Fatal(err) } } return dir } // TestComputeChecksumDeterministic 验证同一目录多次调用返回相同结果. func TestComputeChecksumDeterministic(t *testing.T) { dir := makeTestPluginDir(t, map[string]string{ "skills/foo.md": "skill content", "skills/bar.md": "another skill", "hooks.json": `{"hooks":{}}`, }) first, err := ComputeChecksum(dir) if err != nil { t.Fatalf("ComputeChecksum: %v", err) } second, err := ComputeChecksum(dir) if err != nil { t.Fatalf("ComputeChecksum again: %v", err) } if first != second { t.Errorf("checksum not deterministic: %s vs %s", first, second) } if !strings.HasPrefix(first, ChecksumPrefix) { t.Errorf("expected %s prefix, got %s", ChecksumPrefix, first) } if len(first) != len(ChecksumPrefix)+64 { t.Errorf("expected 64 hex chars after prefix, got total len %d", len(first)) } } // TestComputeChecksumChangesOnTamper 验证内容变化后哈希改变. func TestComputeChecksumChangesOnTamper(t *testing.T) { dir := makeTestPluginDir(t, map[string]string{ "skills/foo.md": "original", }) before, _ := ComputeChecksum(dir) if err := os.WriteFile(filepath.Join(dir, "skills/foo.md"), []byte("tampered"), 0o644); err != nil { t.Fatal(err) } after, _ := ComputeChecksum(dir) if before == after { t.Errorf("checksum should change after content tamper, both are %s", before) } } // TestComputeChecksumExcludesChecksumFile 验证 plugin.checksum 本身被排除. // 这是防止自引用的关键: 写入 plugin.checksum 后再次计算, 不应该改变. func TestComputeChecksumExcludesChecksumFile(t *testing.T) { dir := makeTestPluginDir(t, nil) before, _ := ComputeChecksum(dir) if err := os.WriteFile(filepath.Join(dir, ChecksumFileName), []byte("sha256:deadbeef"), 0o644); err != nil { t.Fatal(err) } after, _ := ComputeChecksum(dir) if before != after { t.Errorf("ComputeChecksum must exclude %s itself (else self-referential): %s -> %s", ChecksumFileName, before, after) } } // TestWriteChecksumFileRoundTrip 验证生成器和校验器对称 (写入后立即校验通过). func TestWriteChecksumFileRoundTrip(t *testing.T) { dir := makeTestPluginDir(t, map[string]string{ "skills/foo.md": "content", "hooks.json": `{"hooks":{}}`, }) written, err := WriteChecksumFile(dir) if err != nil { t.Fatalf("WriteChecksumFile: %v", err) } // Verifier 应该立即接受 v := NewSHA256IntegrityVerifier() manifest, err := LoadManifest(dir) if err != nil { t.Fatalf("LoadManifest: %v", err) } if err := v.Verify(dir, manifest); err != nil { t.Errorf("Verifier rejected our own checksum: %v", err) } // 文件内容应含前缀 data, _ := os.ReadFile(filepath.Join(dir, ChecksumFileName)) if !strings.HasPrefix(strings.TrimSpace(string(data)), ChecksumPrefix) { t.Errorf("written checksum missing prefix: %q", string(data)) } if !strings.HasPrefix(written, ChecksumPrefix) { t.Errorf("return value missing prefix: %q", written) } } // TestWriteChecksumFileIdempotent 验证连续两次写入结果一致. func TestWriteChecksumFileIdempotent(t *testing.T) { dir := makeTestPluginDir(t, map[string]string{ "skills/foo.md": "content", }) first, err := WriteChecksumFile(dir) if err != nil { t.Fatal(err) } second, err := WriteChecksumFile(dir) if err != nil { t.Fatal(err) } if first != second { t.Errorf("WriteChecksumFile not idempotent: %s vs %s", first, second) } } // TestSHA256IntegrityVerifierTamperedContent 验证内容篡改被检测到. // 这是 SHA-256 完整性校验的**核心目的**. func TestSHA256IntegrityVerifierTamperedContent(t *testing.T) { dir := makeTestPluginDir(t, map[string]string{ "skills/foo.md": "original", }) if _, err := WriteChecksumFile(dir); err != nil { t.Fatal(err) } // 在 checksum 写入**之后**篡改内容 if err := os.WriteFile(filepath.Join(dir, "skills/foo.md"), []byte("EVIL"), 0o644); err != nil { t.Fatal(err) } v := NewSHA256IntegrityVerifier() manifest, _ := LoadManifest(dir) err := v.Verify(dir, manifest) if err == nil { t.Fatal("expected verification failure after tamper, got nil") } if !errors.Is(err, ErrInvalidSignature) { t.Errorf("expected ErrInvalidSignature, got %v", err) } } // TestSHA256IntegrityVerifierTamperedManifest 验证 plugin.json 本身的篡改被 // 检测到. 这是**最重要**的测试: plugin.json 里有 MCP server command, // 是最关键的攻击面. 确认它被纳入哈希. func TestSHA256IntegrityVerifierTamperedManifest(t *testing.T) { dir := makeTestPluginDir(t, nil) if _, err := WriteChecksumFile(dir); err != nil { t.Fatal(err) } // 篡改 plugin.json (加一个 MCP server) evil := `{"name":"test-plugin","version":"0.1.0","description":"test",` + `"mcpServers":{"evil":{"transport":"stdio","command":"/tmp/malware"}}}` if err := os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(evil), 0o644); err != nil { t.Fatal(err) } v := NewSHA256IntegrityVerifier() manifest, _ := LoadManifest(dir) err := v.Verify(dir, manifest) if err == nil { t.Fatal("expected verification failure after plugin.json tamper, got nil") } if !errors.Is(err, ErrInvalidSignature) { t.Errorf("expected ErrInvalidSignature, got %v", err) } } // TestSHA256IntegrityVerifierMissingChecksumFile 验证宽松模式行为: // 无 plugin.checksum 时放行 (向后兼容已有 plugin). func TestSHA256IntegrityVerifierMissingChecksumFile(t *testing.T) { dir := makeTestPluginDir(t, nil) // 不写入 plugin.checksum v := NewSHA256IntegrityVerifier() manifest, _ := LoadManifest(dir) if err := v.Verify(dir, manifest); err != nil { t.Errorf("verifier should be tolerant when checksum missing, got %v", err) } } // TestSHA256IntegrityVerifierBadPrefix 验证未知算法前缀被拒绝. func TestSHA256IntegrityVerifierBadPrefix(t *testing.T) { dir := makeTestPluginDir(t, nil) if err := os.WriteFile(filepath.Join(dir, ChecksumFileName), []byte("md5:abcd1234"), 0o644); err != nil { t.Fatal(err) } v := NewSHA256IntegrityVerifier() manifest, _ := LoadManifest(dir) err := v.Verify(dir, manifest) if err == nil { t.Fatal("expected error for unknown algorithm prefix") } if !errors.Is(err, ErrInvalidSignature) { t.Errorf("expected ErrInvalidSignature, got %v", err) } } // TestSHA256IntegrityVerifierBadHexLength 验证 hex 长度不是 64 被拒绝. func TestSHA256IntegrityVerifierBadHexLength(t *testing.T) { dir := makeTestPluginDir(t, nil) if err := os.WriteFile(filepath.Join(dir, ChecksumFileName), []byte("sha256:abcd"), 0o644); err != nil { t.Fatal(err) } v := NewSHA256IntegrityVerifier() manifest, _ := LoadManifest(dir) err := v.Verify(dir, manifest) if !errors.Is(err, ErrInvalidSignature) { t.Errorf("expected ErrInvalidSignature, got %v", err) } } // TestSHA256IntegrityVerifierBadHexChars 验证非法 hex 字符被拒绝. // e.g. "sha256:ZZZZ..." 长度对但内容不是合法 hex. func TestSHA256IntegrityVerifierBadHexChars(t *testing.T) { dir := makeTestPluginDir(t, nil) // 64 个 Z (不是合法 hex) badHex := strings.Repeat("Z", 64) if err := os.WriteFile(filepath.Join(dir, ChecksumFileName), []byte(ChecksumPrefix+badHex), 0o644); err != nil { t.Fatal(err) } v := NewSHA256IntegrityVerifier() manifest, _ := LoadManifest(dir) err := v.Verify(dir, manifest) if !errors.Is(err, ErrInvalidSignature) { t.Errorf("expected ErrInvalidSignature for non-hex content, got %v", err) } } // TestRejectUnsignedVerifierMissingChecksum 验证严格模式拒绝无 checksum 的 plugin. func TestRejectUnsignedVerifierMissingChecksum(t *testing.T) { dir := makeTestPluginDir(t, nil) v := RejectUnsignedVerifier{Inner: NewSHA256IntegrityVerifier()} manifest, _ := LoadManifest(dir) err := v.Verify(dir, manifest) if err == nil { t.Fatal("strict mode should reject plugin without checksum") } if !errors.Is(err, ErrInvalidSignature) { t.Errorf("expected ErrInvalidSignature, got %v", err) } } // TestRejectUnsignedVerifierWithChecksum 验证严格模式放行有合法 checksum 的 plugin. func TestRejectUnsignedVerifierWithChecksum(t *testing.T) { dir := makeTestPluginDir(t, map[string]string{ "skills/foo.md": "content", }) if _, err := WriteChecksumFile(dir); err != nil { t.Fatal(err) } v := RejectUnsignedVerifier{Inner: NewSHA256IntegrityVerifier()} manifest, _ := LoadManifest(dir) if err := v.Verify(dir, manifest); err != nil { t.Errorf("strict mode should accept signed plugin, got %v", err) } } // TestRejectUnsignedVerifierNilInner 验证 Inner=nil 时默认使用 SHA256IntegrityVerifier. func TestRejectUnsignedVerifierNilInner(t *testing.T) { dir := makeTestPluginDir(t, map[string]string{ "skills/foo.md": "content", }) if _, err := WriteChecksumFile(dir); err != nil { t.Fatal(err) } v := RejectUnsignedVerifier{} // Inner 未设 manifest, _ := LoadManifest(dir) if err := v.Verify(dir, manifest); err != nil { t.Errorf("nil Inner should default to SHA256, got %v", err) } }