diff --git a/verifier_tools/verify/cmd/verifier/verifier.go b/verifier_tools/verify/cmd/verifier/verifier.go index 6137047..171ef22 100644 --- a/verifier_tools/verify/cmd/verifier/verifier.go +++ b/verifier_tools/verify/cmd/verifier/verifier.go @@ -42,7 +42,7 @@ const ( KeyNameForVerifierG1PAPK = "gstatic.com/android/binary_transparency/google1p/apk/2026/0" LogBaseURLPixel = "https://developers.google.com/android/binary_transparency" LogBaseURLG1PJWT = "https://developers.google.com/android/binary_transparency/google1p" - LogBaseURLG1PAPK = "https://www.gstatic.com/android/binary_transparency/google1p/apk/2026/01/" + LogBaseURLG1PAPK = "https://www.gstatic.com/android/binary_transparency/google1p/apk/2026/01" ImageInfoFilename = "image_info.txt" PackageInfoFilename = "package_info.txt" ) @@ -127,7 +127,9 @@ func main() { os.Exit(1) } - m, err := tiles.BinaryInfosIndex(logBaseURL, binaryInfoFilename) + logSize := int64(root.Size) + + m, err := tiles.BinaryInfosIndex(logBaseURL, binaryInfoFilename, logSize) if err != nil { slog.Error("failed to load binary info map to find log index", "error", err) os.Exit(1) @@ -141,7 +143,6 @@ func main() { var th tlog.Hash copy(th[:], root.Hash) - logSize := int64(root.Size) r := tiles.HashReader{URL: logBaseURL, TileHeight: tileHeight, TreeSize: logSize} slog.Debug("tlog.ProveRecord", "logSize", logSize, "binaryInfoIndex", binaryInfoIndex) rp, err := tlog.ProveRecord(logSize, binaryInfoIndex, r) diff --git a/verifier_tools/verify/internal/tiles/reader.go b/verifier_tools/verify/internal/tiles/reader.go index faa79c9..8ab4ec1 100644 --- a/verifier_tools/verify/internal/tiles/reader.go +++ b/verifier_tools/verify/internal/tiles/reader.go @@ -8,9 +8,12 @@ import ( "log/slog" "net/http" "net/url" + "os" "path" + "path/filepath" "strconv" "strings" + "time" "golang.org/x/mod/sumdb/tlog" ) @@ -96,8 +99,8 @@ func (h HashReader) ReadHashes(indices []int64) ([]tlog.Hash, error) { // BinaryInfosIndex returns a map from payload to its index in the // transparency log according to the `binaryInfoFilename` value. -func BinaryInfosIndex(logBaseURL string, binaryInfoFilename string) (map[string]int64, error) { - b, err := readFromURL(logBaseURL, binaryInfoFilename) +func BinaryInfosIndex(logBaseURL string, binaryInfoFilename string, treeSize int64) (map[string]int64, error) { + b, err := readCachedInfoFile(logBaseURL, binaryInfoFilename, treeSize) if err != nil { return nil, err } @@ -106,6 +109,113 @@ func BinaryInfosIndex(logBaseURL string, binaryInfoFilename string) (map[string] return parseBinaryInfosIndex(binaryInfos, binaryInfoFilename) } +func readCachedInfoFile(logBaseURL string, binaryInfoFilename string, treeSize int64) ([]byte, error) { + cacheDir, err := os.UserCacheDir() + if err != nil { + slog.Warn("Failed to get user cache dir, falling back to direct download", "error", err) + return readFromURL(logBaseURL, binaryInfoFilename) + } + + abtCacheDir := filepath.Join(cacheDir, "android-binary-transparency") + if err := os.MkdirAll(abtCacheDir, 0755); err != nil { + slog.Warn("Failed to create cache dir, falling back to direct download", "error", err) + return readFromURL(logBaseURL, binaryInfoFilename) + } + + urlHash := sha256.Sum256([]byte(logBaseURL)) + basePrefix := fmt.Sprintf("%x_%s", urlHash[:8], binaryInfoFilename) + cacheFilename := fmt.Sprintf("%s_%d", basePrefix, treeSize) + cachePath := filepath.Join(abtCacheDir, cacheFilename) + + // Try reading from cache + if b, err := os.ReadFile(cachePath); err == nil { + slog.Debug("Loaded info file from local cache", "path", cachePath) + return b, nil + } + + // Cache miss, download from URL + slog.Info("Downloading new info file", "url", logBaseURL+"/"+binaryInfoFilename) + b, err := readFromURL(logBaseURL, binaryInfoFilename) + if err != nil { + return nil, err + } + + // Save to cache atomically + tmpFile, err := os.CreateTemp(abtCacheDir, cacheFilename+".*.tmp") + if err != nil { + slog.Warn("Failed to create cache tmp file", "error", err) + return b, nil + } + tmpPath := tmpFile.Name() + + // Clean up tmp file on exit if it hasn't been renamed + defer os.Remove(tmpPath) + + slog.Info("Writing info file to cache", "path", tmpPath) + if _, err := tmpFile.Write(b); err != nil { + slog.Warn("Failed to write to cache tmp file", "error", err) + tmpFile.Close() + return b, nil + } + if err := tmpFile.Close(); err != nil { + slog.Warn("Failed to close cache tmp file", "error", err) + return b, nil + } + + slog.Info("Renaming cache file", "from", tmpPath, "to", cachePath) + if err := os.Rename(tmpPath, cachePath); err != nil { + slog.Warn("Failed to move cache file to final destination", "error", err) + return b, nil + } + + slog.Debug("Saved info file to local cache", "path", cachePath) + + // Cleanup old cache files for this specific log URL and filename safely + slog.Info("Cleaning up old cache files", "prefix", basePrefix) + if entries, err := os.ReadDir(abtCacheDir); err == nil { + for _, entry := range entries { + if entry.IsDir() { + continue + } + + // Only process files that match our specific basePrefix + if !strings.HasPrefix(entry.Name(), basePrefix+"_") { + continue + } + + f := filepath.Join(abtCacheDir, entry.Name()) + if f == cachePath { + continue + } + + info, err := entry.Info() + if err != nil { + continue + } + + // Delete old temp files (older than 1 hour) left over from hard crashes. + // Otherwise, keep current temp files to avoid breaking active concurrent downloads. + if strings.HasSuffix(entry.Name(), ".tmp") { + if time.Since(info.ModTime()) > time.Hour { + if err := os.Remove(f); err == nil { + slog.Debug("Cleaned up orphaned cache temp file", "path", f) + } + } + continue + } + + // Delete old cache files (older than 24 hours to prevent cache invalidation storms) + if time.Since(info.ModTime()) > 24*time.Hour { + if err := os.Remove(f); err == nil { + slog.Debug("Cleaned up old cache file", "path", f) + } + } + } + } + + return b, nil +} + func parseBinaryInfosIndex(binaryInfos string, binaryInfoFilename string) (map[string]int64, error) { m := make(map[string]int64) diff --git a/verifier_tools/verify/internal/tiles/reader_test.go b/verifier_tools/verify/internal/tiles/reader_test.go index ecca52e..2445d7a 100644 --- a/verifier_tools/verify/internal/tiles/reader_test.go +++ b/verifier_tools/verify/internal/tiles/reader_test.go @@ -93,7 +93,7 @@ func TestReadHashesWithReadTileData(t *testing.T) { }, } { t.Run(tc.desc, func(t *testing.T) { - r := HashReader{URL: s.URL} + r := HashReader{URL: s.URL, TileHeight: tileHeight, TreeSize: int64(tc.size)} // Read hashes. for i, want := range tc.want { @@ -116,7 +116,7 @@ func TestReadHashesCachedTile(t *testing.T) { defer s.Close() wantHash := nodeHashes[0][0] - r := HashReader{URL: s.URL} + r := HashReader{URL: s.URL, TileHeight: tileHeight, TreeSize: 3} // Read hash at index 0 twice, to exercise the caching of tiles. // On the first pass, the read is fresh and readFromURL is called.