// Command ctfetch fetches and dumps entries from a Static CT log. // // Two modes: // // ctfetch [flags] [+sct] [+issuer] [+ctlog] fetch one entry by leaf index // ctfetch [flags] [+sct] [+issuer] [+ctlog] dump all entries in a tile // // (C) Copyright 2026 Pim van Pelt package main import ( "encoding/json" "flag" "fmt" "os" "strconv" "strings" "ctfetch/internal/utils" "filippo.io/sunlight" "golang.org/x/mod/sumdb/tlog" ) func main() { logsListURL := flag.String("logs-list-url", "https://www.gstatic.com/ct/log_list/v3/all_logs_list.json", "URL of the CT log list JSON") monitoringURL := flag.String("monitoring-url", "", "log root URL for issuer lookups when input is a file") flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage:\n") fmt.Fprintf(os.Stderr, " %s [flags] [+sct] [+issuer] [+ctlog] fetch one entry\n", os.Args[0]) fmt.Fprintf(os.Stderr, " %s [flags] [+sct] [+issuer] [+ctlog] dump all entries in a tile\n", os.Args[0]) fmt.Fprintf(os.Stderr, "\nExamples:\n") fmt.Fprintf(os.Stderr, " %s https://halloumi2026h1.mon.ct.ipng.ch 457683896 +sct +issuer +ctlog\n", os.Args[0]) fmt.Fprintf(os.Stderr, " %s https://halloumi2026h1.mon.ct.ipng.ch/tile/data/x002/x460/135 +sct\n", os.Args[0]) fmt.Fprintf(os.Stderr, " %s --monitoring-url https://halloumi2026h1.mon.ct.ipng.ch tile.bin +issuer\n", os.Args[0]) fmt.Fprintf(os.Stderr, "\nFlags:\n") flag.PrintDefaults() } flag.Parse() if flag.NArg() < 1 { flag.Usage() os.Exit(1) } // Determine mode: if second positional arg parses as an integer → leaf-index mode. _, secondIsInt := func() (int64, bool) { if flag.NArg() < 2 { return 0, false } v, err := strconv.ParseInt(flag.Arg(1), 10, 64) return v, err == nil }() var modifiers []string if secondIsInt { modifiers = flag.Args()[2:] } else { modifiers = flag.Args()[1:] } opts := utils.Options{} for _, arg := range modifiers { switch arg { case "+sct": opts.ShowSCT = true case "+issuer": opts.ShowIssuer = true case "+ctlog": opts.ShowCTLog = true default: fatal("unknown argument %q (expected +sct, +issuer, or +ctlog)", arg) } } if opts.ShowCTLog && *logsListURL != "" { ctlogs, err := utils.FetchCTLogList(*logsListURL) if err != nil { fmt.Fprintf(os.Stderr, "WARNING: could not fetch CT log list: %v\n", err) } else { opts.CTLogs = ctlogs } } if secondIsInt { runLeafIndex(flag.Arg(0), flag.Arg(1), opts) } else { runTileDump(flag.Arg(0), *monitoringURL, opts) } } func runLeafIndex(logURL, indexStr string, opts utils.Options) { logURL = strings.TrimSuffix(logURL, "/") opts.LogURL = logURL leafIndex, err := strconv.ParseInt(indexStr, 10, 64) if err != nil { fatal("invalid leaf index: %v", err) } tile := tlog.TileForIndex(sunlight.TileHeight, tlog.StoredHashIndex(0, leafIndex)) tile.L = -1 partialPath := sunlight.TilePath(tile) positionInTile := leafIndex % sunlight.TileWidth tileData, err := utils.FetchTile(logURL + "/" + partialPath) if err != nil { fatal("failed to fetch tile: %v", err) } tileData, err = utils.Decompress(tileData) if err != nil { fatal("failed to decompress tile: %v", err) } entry, err := utils.DumpEntryAtPosition(tileData, int(positionInTile), leafIndex, opts) if err != nil { fatal("%v", err) } printJSON(entry) } func runTileDump(arg, monitoringURL string, opts utils.Options) { var tileData []byte var err error if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") { // Derive log root from tile URL for issuer lookups. if opts.ShowIssuer { if idx := strings.Index(arg, "/tile/"); idx != -1 { opts.LogURL = strings.TrimSuffix(arg[:idx], "/") } else if monitoringURL != "" { opts.LogURL = strings.TrimSuffix(monitoringURL, "/") } else { fatal("+issuer requires a log root URL; none could be derived from %q and --monitoring-url is not set", arg) } } tileData, err = utils.FetchTile(arg) if err != nil { fatal("failed to fetch tile: %v", err) } } else { // File input. if opts.ShowIssuer { if monitoringURL != "" { opts.LogURL = strings.TrimSuffix(monitoringURL, "/") } else { fatal("+issuer requires --monitoring-url when input is a file") } } tileData, err = os.ReadFile(arg) if err != nil { fatal("failed to read file: %v", err) } } tileData, err = utils.Decompress(tileData) if err != nil { fatal("failed to decompress tile: %v", err) } result, err := utils.DumpAllEntries(tileData, opts) if err != nil { fatal("%v", err) } printJSON(result) } func printJSON(v interface{}) { data, err := json.MarshalIndent(v, "", " ") if err != nil { fatal("failed to marshal JSON: %v", err) } fmt.Println(string(data)) } func fatal(format string, args ...any) { fmt.Fprintf(os.Stderr, "Error: "+format+"\n", args...) os.Exit(1) }