// Package manifest verifies downloaded Mithril snapshot files against the // per-immutable digest manifest extracted from the snapshot's `digests` // archive. // // The manifest is a flat JSON array of {immutable_file_name, digest} // entries — one per file in the chain DB's immutable directory. Each // digest is the SHA-256 of the file contents. package manifest import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "os" "path/filepath" "sort" "strings" ) // Entry is one record from the digests JSON. type Entry struct { ImmutableFileName string `json:"immutable_file_name"` Digest string `json:"digest"` // hex-encoded SHA-256 } // Load reads + parses a digests.json file. func Load(path string) ([]Entry, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() var out []Entry if err := json.NewDecoder(f).Decode(&out); err != nil { return nil, err } return out, nil } // LocateDigests finds the digests.json file inside the extracted digests // directory (named like `-e-i.digests.json`). func LocateDigests(digestsDir string) (string, error) { entries, err := os.ReadDir(digestsDir) if err != nil { return "", err } for _, e := range entries { if !e.IsDir() && strings.HasSuffix(e.Name(), ".digests.json") { return filepath.Join(digestsDir, e.Name()), nil } } return "", fmt.Errorf("no digests.json found under %s", digestsDir) } // Result captures the per-file outcome of a manifest verification pass. type Result struct { TotalEntries int `json:"total_entries"` Verified int `json:"verified"` Missing []string `json:"missing,omitempty"` // files in manifest, not on disk Mismatched []string `json:"mismatched,omitempty"` // SHA didn't match Unverifiable []string `json:"unverifiable,omitempty"` // failed to read Extra []string `json:"extra,omitempty"` // files on disk, not in manifest OK bool `json:"ok"` } // Verify walks the manifest and checks each immutable file under dbDir. // Files in the immutable subdirectory are at `/immutable/`. func Verify(entries []Entry, dbDir string) (*Result, error) { r := &Result{TotalEntries: len(entries)} immDir := filepath.Join(dbDir, "immutable") // Build set of expected names for the extra-file diff. expected := make(map[string]struct{}, len(entries)) for _, e := range entries { expected[e.ImmutableFileName] = struct{}{} } for _, e := range entries { path := filepath.Join(immDir, e.ImmutableFileName) fi, err := os.Stat(path) if err != nil { if os.IsNotExist(err) { r.Missing = append(r.Missing, e.ImmutableFileName) continue } r.Unverifiable = append(r.Unverifiable, e.ImmutableFileName+": "+err.Error()) continue } if fi.IsDir() { r.Unverifiable = append(r.Unverifiable, e.ImmutableFileName+": is a directory") continue } got, err := sha256OfFile(path) if err != nil { r.Unverifiable = append(r.Unverifiable, e.ImmutableFileName+": "+err.Error()) continue } if got != e.Digest { r.Mismatched = append(r.Mismatched, fmt.Sprintf("%s: got %s, want %s", e.ImmutableFileName, got, e.Digest)) continue } r.Verified++ } // Identify any extra files in immutable/ that aren't in the manifest. if entries, err := os.ReadDir(immDir); err == nil { for _, fe := range entries { if fe.IsDir() { continue } if _, ok := expected[fe.Name()]; !ok { r.Extra = append(r.Extra, fe.Name()) } } } sort.Strings(r.Missing) sort.Strings(r.Mismatched) sort.Strings(r.Unverifiable) sort.Strings(r.Extra) r.OK = len(r.Missing) == 0 && len(r.Mismatched) == 0 && len(r.Unverifiable) == 0 return r, nil } func sha256OfFile(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return "", err } return hex.EncodeToString(h.Sum(nil)), nil }