Add truth package verifier evidence gate

This commit is contained in:
Codex
2026-06-08 23:45:07 +02:00
parent 2344a852b5
commit d6448af1ec
2 changed files with 300 additions and 0 deletions

View File

@@ -1,8 +1,10 @@
package main package main
import ( import (
"archive/zip"
"bufio" "bufio"
"context" "context"
"crypto/sha256"
"encoding/json" "encoding/json"
"errors" "errors"
"flag" "flag"
@@ -29,6 +31,7 @@ var supportedVerifyKindNames = []string{
"anchor", "anchor",
"ivc", "ivc",
"bundle", "bundle",
"truth-package",
} }
type cliConfig struct { type cliConfig struct {
@@ -124,6 +127,8 @@ func (a *app) dispatch(ctx context.Context, args []string) error {
return a.verifyableObject(ctx, "anchors", args[1:], attesto.VerifyAnchor, "/v2/anchors/") return a.verifyableObject(ctx, "anchors", args[1:], attesto.VerifyAnchor, "/v2/anchors/")
case "bundles": case "bundles":
return a.bundles(ctx, args[1:]) return a.bundles(ctx, args[1:])
case "verify":
return a.verify(ctx, args[1:])
case "fork-evidence": case "fork-evidence":
return a.forkEvidence(ctx, args[1:]) return a.forkEvidence(ctx, args[1:])
case "quorum": case "quorum":
@@ -143,6 +148,28 @@ func (a *app) dispatch(ctx context.Context, args []string) error {
} }
} }
func (a *app) verify(ctx context.Context, args []string) error {
if len(args) == 0 {
return errors.New("verify subcommand required")
}
switch args[0] {
case "truth-package":
fs := flag.NewFlagSet("verify truth-package", flag.ContinueOnError)
fs.SetOutput(a.err)
file := fs.String("file", "", "truth package ZIP file")
if err := fs.Parse(args[1:]); err != nil {
return err
}
if *file == "" {
return errors.New("--file is required")
}
return a.write(verifyTruthPackageZip(*file))
default:
_ = ctx
return fmt.Errorf("unknown verify subcommand: %s", args[0])
}
}
func (a *app) config(args []string) error { func (a *app) config(args []string) error {
if len(args) == 0 { if len(args) == 0 {
return errors.New("config subcommand required") return errors.New("config subcommand required")
@@ -1315,6 +1342,138 @@ func verifyBundleHash(obj attesto.M) map[string]any {
return verifyDomainObject("bundle", obj, "payload", "bundleHash", "bundle_hash") return verifyDomainObject("bundle", obj, "payload", "bundleHash", "bundle_hash")
} }
func verifyTruthPackageZip(path string) map[string]any {
problems := []string{}
raw, err := os.ReadFile(path)
if err != nil {
return map[string]any{"ok": false, "problems": []string{err.Error()}}
}
packageHash := fmt.Sprintf("%x", sha256.Sum256(raw))
reader, err := zip.OpenReader(path)
if err != nil {
return map[string]any{"ok": false, "packageSha256": packageHash, "problems": []string{err.Error()}}
}
defer reader.Close()
entries := map[string]*zip.File{}
for _, item := range reader.File {
name := item.Name
if truthPackageForbiddenZipPath(name) {
problems = append(problems, "forbidden ZIP entry: "+name)
continue
}
entries[name] = item
}
manifestFile := entries["attesto.truth-package.manifest.json"]
if manifestFile == nil {
problems = append(problems, "attesto.truth-package.manifest.json missing")
return map[string]any{"ok": false, "packageSha256": packageHash, "problems": problems}
}
manifestBytes, err := readZipFile(manifestFile)
if err != nil {
problems = append(problems, "truth package manifest read failed: "+err.Error())
return map[string]any{"ok": false, "packageSha256": packageHash, "problems": problems}
}
manifestHash := fmt.Sprintf("%x", sha256.Sum256(manifestBytes))
var manifest map[string]any
if err := json.Unmarshal(manifestBytes, &manifest); err != nil {
problems = append(problems, "truth package manifest is not JSON: "+err.Error())
return map[string]any{"ok": false, "packageSha256": packageHash, "manifestSha256": manifestHash, "problems": problems}
}
if stringField(manifest, "schema") != "attesto.truth-package.manifest.v1" {
problems = append(problems, "truth package manifest schema mismatch")
}
verifiedArtifacts := 0
allowedEntries := map[string]bool{
"attesto.truth-package.manifest.json": true,
"attesto.sig.json": true,
}
if artifacts, ok := manifest["artifacts"].([]any); ok {
for _, artifact := range artifacts {
row, ok := artifact.(map[string]any)
if !ok {
problems = append(problems, "artifact entry is not an object")
continue
}
artifactPath := stringField(row, "path")
expectedHash := stringField(row, "sha256")
if artifactPath == "" || expectedHash == "" {
problems = append(problems, "artifact path or sha256 missing")
continue
}
allowedEntries[artifactPath] = true
if truthPackageForbiddenZipPath(artifactPath) {
problems = append(problems, "forbidden artifact path: "+artifactPath)
continue
}
entry := entries[artifactPath]
if entry == nil {
problems = append(problems, "artifact missing from ZIP: "+artifactPath)
continue
}
data, err := readZipFile(entry)
if err != nil {
problems = append(problems, "artifact read failed: "+artifactPath)
continue
}
computed := fmt.Sprintf("%x", sha256.Sum256(data))
if computed != expectedHash {
problems = append(problems, "artifact sha256 mismatch: "+artifactPath)
continue
}
verifiedArtifacts++
}
} else {
problems = append(problems, "truth package artifacts list missing")
}
for entryName := range entries {
if !allowedEntries[entryName] {
problems = append(problems, "unlisted ZIP entry: "+entryName)
}
}
return map[string]any{
"ok": len(problems) == 0,
"packageSha256": packageHash,
"manifestSha256": manifestHash,
"verifiedArtifacts": verifiedArtifacts,
"problems": problems,
}
}
func readZipFile(file *zip.File) ([]byte, error) {
reader, err := file.Open()
if err != nil {
return nil, err
}
defer reader.Close()
return io.ReadAll(reader)
}
func truthPackageForbiddenZipPath(path string) bool {
clean := filepath.Clean(path)
if path == "" ||
strings.HasPrefix(path, "/") ||
strings.Contains(path, "\\") ||
strings.Contains(path, ":") ||
strings.HasPrefix(path, "__MACOSX/") ||
strings.Contains(path, "/__MACOSX/") ||
strings.HasSuffix(path, ".DS_Store") ||
strings.HasPrefix(filepath.Base(path), "._") ||
strings.Contains(path, "sftp://") ||
strings.Contains(path, "/home/") ||
clean == "." ||
strings.HasPrefix(clean, "../") ||
clean == ".." {
return true
}
for _, segment := range strings.Split(path, "/") {
if segment == ".." {
return true
}
}
return false
}
func verifyDomainObject(domainKey string, obj attesto.M, payloadKey, camelHash, snakeHash string) map[string]any { func verifyDomainObject(domainKey string, obj attesto.M, payloadKey, camelHash, snakeHash string) map[string]any {
payload := objectField(obj, payloadKey) payload := objectField(obj, payloadKey)
hash := stringField(obj, camelHash, snakeHash) hash := stringField(obj, camelHash, snakeHash)

View File

@@ -1,8 +1,11 @@
package main package main
import ( import (
"archive/zip"
"bytes" "bytes"
"crypto/sha256"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
@@ -51,6 +54,85 @@ func TestReceiptsVerifyOfflineGoldenVector(t *testing.T) {
} }
} }
func TestVerifyTruthPackageZip(t *testing.T) {
dir := t.TempDir()
zipFile := filepath.Join(dir, "truth-package.zip")
writeTruthPackageZip(t, zipFile, false)
var stdout, stderr bytes.Buffer
code := run([]string{"--json", "verify", "truth-package", "--file", zipFile}, &stdout, &stderr, testEnv(t, nil))
if code != 0 {
t.Fatalf("exit=%d stderr=%s", code, stderr.String())
}
var out map[string]any
if err := json.Unmarshal(stdout.Bytes(), &out); err != nil {
t.Fatalf("json: %v", err)
}
if out["ok"] != true {
t.Fatalf("truth package did not verify: %s", stdout.String())
}
if out["packageSha256"] == "" || out["manifestSha256"] == "" {
t.Fatalf("missing hashes: %s", stdout.String())
}
}
func TestVerifyTruthPackageZipRejectsTamperedArtifact(t *testing.T) {
dir := t.TempDir()
zipFile := filepath.Join(dir, "truth-package-tampered.zip")
writeTruthPackageZip(t, zipFile, true)
var stdout, stderr bytes.Buffer
code := run([]string{"--json", "verify", "truth-package", "--file", zipFile}, &stdout, &stderr, testEnv(t, nil))
if code != 0 {
t.Fatalf("exit=%d stderr=%s", code, stderr.String())
}
if !strings.Contains(stdout.String(), `"ok": false`) || !strings.Contains(stdout.String(), "artifact sha256 mismatch") {
t.Fatalf("expected artifact mismatch: %s", stdout.String())
}
}
func TestVerifyTruthPackageZipRejectsForbiddenPath(t *testing.T) {
dir := t.TempDir()
zipFile := filepath.Join(dir, "truth-package-forbidden.zip")
out, err := os.Create(zipFile)
if err != nil {
t.Fatal(err)
}
archive := zip.NewWriter(out)
writeZipEntry(t, archive, "../evil.txt", []byte("x"))
writeZipEntry(t, archive, "attesto.truth-package.manifest.json", []byte(`{"schema":"attesto.truth-package.manifest.v1","artifacts":[]}`))
if err := archive.Close(); err != nil {
t.Fatal(err)
}
if err := out.Close(); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
code := run([]string{"--json", "verify", "truth-package", "--file", zipFile}, &stdout, &stderr, testEnv(t, nil))
if code != 0 {
t.Fatalf("exit=%d stderr=%s", code, stderr.String())
}
if !strings.Contains(stdout.String(), `"ok": false`) || !strings.Contains(stdout.String(), "forbidden ZIP entry") {
t.Fatalf("expected forbidden ZIP entry rejection: %s", stdout.String())
}
}
func TestVerifyTruthPackageZipRejectsUnlistedEntry(t *testing.T) {
dir := t.TempDir()
zipFile := filepath.Join(dir, "truth-package-unlisted.zip")
writeTruthPackageZip(t, zipFile, false, "extra.txt")
var stdout, stderr bytes.Buffer
code := run([]string{"--json", "verify", "truth-package", "--file", zipFile}, &stdout, &stderr, testEnv(t, nil))
if code != 0 {
t.Fatalf("exit=%d stderr=%s", code, stderr.String())
}
if !strings.Contains(stdout.String(), `"ok": false`) || !strings.Contains(stdout.String(), "unlisted ZIP entry") {
t.Fatalf("expected unlisted ZIP entry rejection: %s", stdout.String())
}
}
func TestConfigSetRedactsSecrets(t *testing.T) { func TestConfigSetRedactsSecrets(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
config := filepath.Join(dir, "config.json") config := filepath.Join(dir, "config.json")
@@ -268,6 +350,65 @@ func writeMarketplaceManifest(t *testing.T, path string) {
} }
} }
func writeTruthPackageZip(t *testing.T, path string, tamper bool, extraEntries ...string) {
t.Helper()
events := []byte("id,type\nsev_1,inference\n")
proofs := []byte("{}\n")
manifest := []byte(`{"export":{"id":"exp_test"}}` + "\n")
artifacts := []map[string]any{
{"path": "events.csv", "sha256": sha256Hex(events), "media_type": "text/csv", "size_bytes": len(events)},
{"path": "proofs.json", "sha256": sha256Hex(proofs), "media_type": "application/json", "size_bytes": len(proofs)},
{"path": "manifest.json", "sha256": sha256Hex(manifest), "media_type": "application/json", "size_bytes": len(manifest)},
}
truthManifestRaw, err := json.MarshalIndent(map[string]any{
"schema": "attesto.truth-package.manifest.v1",
"package_id": "tp_test",
"export_id": "exp_test",
"tenant_id": "tnt_test",
"package_version": "attesto.truth-package.v1",
"created_at": "2026-06-08T22:20:00Z",
"artifacts": artifacts,
}, "", " ")
if err != nil {
t.Fatal(err)
}
out, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
defer out.Close()
archive := zip.NewWriter(out)
writeZipEntry(t, archive, "events.csv", events)
if tamper {
writeZipEntry(t, archive, "proofs.json", []byte(`{"tampered":true}`+"\n"))
} else {
writeZipEntry(t, archive, "proofs.json", proofs)
}
writeZipEntry(t, archive, "manifest.json", manifest)
writeZipEntry(t, archive, "attesto.truth-package.manifest.json", append(truthManifestRaw, '\n'))
for _, extraEntry := range extraEntries {
writeZipEntry(t, archive, extraEntry, []byte("not listed\n"))
}
if err := archive.Close(); err != nil {
t.Fatal(err)
}
}
func writeZipEntry(t *testing.T, archive *zip.Writer, name string, data []byte) {
t.Helper()
writer, err := archive.Create(name)
if err != nil {
t.Fatal(err)
}
if _, err := writer.Write(data); err != nil {
t.Fatal(err)
}
}
func sha256Hex(data []byte) string {
return fmt.Sprintf("%x", sha256.Sum256(data))
}
func testEnv(t *testing.T, values map[string]string) func(string) string { func testEnv(t *testing.T, values map[string]string) func(string) string {
t.Helper() t.Helper()
return func(key string) string { return func(key string) string {