diff --git a/.gitignore b/.gitignore index 45d19a4..38ae3d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -/ctfetch -/ctail +/ctool diff --git a/README.md b/README.md index 42d6155..3c0eb64 100644 --- a/README.md +++ b/README.md @@ -5,27 +5,29 @@ Tools for working with [Static CT log](https://c2sp.org/static-ct-api) tiles. ## Install ```bash -GOPRIVATE=git.ipng.ch go install git.ipng.ch/certificate-transparency/ctfetch/cmd/...@latest +GOPRIVATE=git.ipng.ch go install git.ipng.ch/certificate-transparency/ctfetch/cmd/ctool@latest ``` -## Tools +The `GOPRIVATE` variable skips the Go checksum database and module proxy, which do not index modules on `git.ipng.ch`. -### ctfetch +## Commands + +### ctool fetch Fetch and decode entries from a Static CT log as structured JSON. ```bash -ctfetch https://halloumi2026h1.mon.ct.ipng.ch 629794635 +all +ctool fetch https://halloumi2026h1.mon.ct.ipng.ch 629794635 +all ``` → [Full documentation](docs/ctfetch.md) -### ctail +### ctool tail Tail a Static CT log, printing a one-liner per new cert/precert as it arrives. ```bash -ctail https://halloumi2026h1.mon.ct.ipng.ch +ctool tail https://halloumi2026h1.mon.ct.ipng.ch ``` → [Full documentation](docs/ctail.md) diff --git a/cmd/ctfetch/main.go b/cmd/ctool/cmd_fetch.go similarity index 66% rename from cmd/ctfetch/main.go rename to cmd/ctool/cmd_fetch.go index 2c60e0e..3207bfe 100644 --- a/cmd/ctfetch/main.go +++ b/cmd/ctool/cmd_fetch.go @@ -1,11 +1,3 @@ -// Command ctfetch fetches and dumps entries from a Static CT log. -// -// Two modes: -// -// ctfetch [flags] [+sct] [+issuer] [+ctlog] [+all] fetch one entry by leaf index -// ctfetch [flags] [+sct] [+issuer] [+ctlog] [+all] dump all entries in a tile -// -// (C) Copyright 2026 Pim van Pelt package main import ( @@ -16,47 +8,52 @@ import ( "strconv" "strings" - "git.ipng.ch/certificate-transparency/ctfetch/internal/utils" - "filippo.io/sunlight" + "git.ipng.ch/certificate-transparency/ctfetch/internal/utils" "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() { +func runFetch(args []string) { + fs := flag.NewFlagSet("fetch", flag.ContinueOnError) + logsListURL := fs.String("logs-list-url", "https://www.gstatic.com/ct/log_list/v3/all_logs_list.json", "URL of the CT log list JSON") + monitoringURL := fs.String("monitoring-url", "", "log root URL for issuer lookups when input is a file") + fs.Usage = func() { fmt.Fprintf(os.Stderr, "Usage:\n") - fmt.Fprintf(os.Stderr, " %s [flags] [+sct] [+issuer] [+ctlog] [+all] fetch one entry\n", os.Args[0]) - fmt.Fprintf(os.Stderr, " %s [flags] [+sct] [+issuer] [+ctlog] [+all] dump all entries in a tile\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " ctool fetch [flags] [+sct] [+issuer] [+ctlog] [+all] fetch one entry\n") + fmt.Fprintf(os.Stderr, " ctool fetch [flags] [+sct] [+issuer] [+ctlog] [+all] dump all entries in a tile\n") 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, " ctool fetch https://halloumi2026h1.mon.ct.ipng.ch 629794635 +all\n") + fmt.Fprintf(os.Stderr, " ctool fetch https://halloumi2026h1.mon.ct.ipng.ch/tile/data/x002/x460/135 +sct\n") fmt.Fprintf(os.Stderr, "\nFlags:\n") - flag.PrintDefaults() + fs.PrintDefaults() } - flag.Parse() - if flag.NArg() < 1 { - flag.Usage() + if err := fs.Parse(args); err != nil { + if err == flag.ErrHelp { + os.Exit(0) + } + os.Exit(1) + } + + if fs.NArg() < 1 { + fs.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 { + if fs.NArg() < 2 { return 0, false } - v, err := strconv.ParseInt(flag.Arg(1), 10, 64) + v, err := strconv.ParseInt(fs.Arg(1), 10, 64) return v, err == nil }() var modifiers []string if secondIsInt { - modifiers = flag.Args()[2:] + modifiers = fs.Args()[2:] } else { - modifiers = flag.Args()[1:] + modifiers = fs.Args()[1:] } opts := utils.Options{} @@ -87,9 +84,9 @@ func main() { } if secondIsInt { - runLeafIndex(flag.Arg(0), flag.Arg(1), opts) + runLeafIndex(fs.Arg(0), fs.Arg(1), opts) } else { - runTileDump(flag.Arg(0), *monitoringURL, opts) + runTileDump(fs.Arg(0), *monitoringURL, opts) } } @@ -104,16 +101,13 @@ func runLeafIndex(logURL, indexStr string, opts utils.Options) { 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) @@ -131,7 +125,6 @@ func runTileDump(arg, monitoringURL string, opts utils.Options) { 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], "/") @@ -146,7 +139,6 @@ func runTileDump(arg, monitoringURL string, opts utils.Options) { fatal("failed to fetch tile: %v", err) } } else { - // File input. if opts.ShowIssuer { if monitoringURL != "" { opts.LogURL = strings.TrimSuffix(monitoringURL, "/") @@ -179,8 +171,3 @@ func printJSON(v interface{}) { } fmt.Println(string(data)) } - -func fatal(format string, args ...any) { - fmt.Fprintf(os.Stderr, "Error: "+format+"\n", args...) - os.Exit(1) -} diff --git a/cmd/ctail/main.go b/cmd/ctool/cmd_tail.go similarity index 84% rename from cmd/ctail/main.go rename to cmd/ctool/cmd_tail.go index f43fc5b..95fd527 100644 --- a/cmd/ctail/main.go +++ b/cmd/ctool/cmd_tail.go @@ -1,8 +1,3 @@ -// Command ctail tails a Static CT log, printing a one-liner per new entry. -// -// ctail [flags] -// -// (C) Copyright 2026 Pim van Pelt package main import ( @@ -23,29 +18,34 @@ import ( "golang.org/x/mod/sumdb/tlog" ) -const version = "0.1.0" - var ( userAgent string rateLimit time.Duration lastRequest time.Time ) -func main() { - interval := flag.Duration("interval", 15*time.Second, "polling interval") - fromLeaf := flag.Int64("from-leaf", -1, "start from this leaf index (-1 = current tree tip)") - rateLimitFlag := flag.Duration("rate-limit", 2*time.Second, "minimum time between HTTP requests") - flag.StringVar(&userAgent, "user-agent", "ctail/"+version+" (https://git.ipng.ch/certificate-transparency/)", "User-Agent header for HTTP requests") - flag.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage: ctail [flags] \n") +func runTail(args []string) { + fs := flag.NewFlagSet("tail", flag.ContinueOnError) + interval := fs.Duration("interval", 15*time.Second, "polling interval (minimum 1s)") + fromLeaf := fs.Int64("from-leaf", -1, "start from this leaf index (-1 = current tree tip)") + rateLimitFlag := fs.Duration("rate-limit", 2*time.Second, "minimum time between HTTP requests (minimum 100ms)") + fs.StringVar(&userAgent, "user-agent", "ctool/"+version+" (https://git.ipng.ch/certificate-transparency/)", "User-Agent header for HTTP requests") + fs.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: ctool tail [flags] \n") fmt.Fprintf(os.Stderr, "\nPrints a one-liner per cert/pre-cert as new entries arrive in a Static CT log.\n") fmt.Fprintf(os.Stderr, "\nExamples:\n") - fmt.Fprintf(os.Stderr, " ctail https://halloumi2026h1.mon.ct.ipng.ch\n") - fmt.Fprintf(os.Stderr, " ctail --from-leaf 0 --interval 10s https://halloumi2026h1.mon.ct.ipng.ch\n") + fmt.Fprintf(os.Stderr, " ctool tail https://halloumi2026h2.mon.ct.ipng.ch\n") + fmt.Fprintf(os.Stderr, " ctool tail --from-leaf 0 --interval 10s https://halloumi2026h2.mon.ct.ipng.ch\n") fmt.Fprintf(os.Stderr, "\nFlags:\n") - flag.PrintDefaults() + fs.PrintDefaults() + } + + if err := fs.Parse(args); err != nil { + if err == flag.ErrHelp { + os.Exit(0) + } + os.Exit(1) } - flag.Parse() if *interval < time.Second { fmt.Fprintf(os.Stderr, "Error: --interval must be at least 1s\n") @@ -57,12 +57,12 @@ func main() { } rateLimit = *rateLimitFlag - if flag.NArg() != 1 { - flag.Usage() + if fs.NArg() != 1 { + fs.Usage() os.Exit(1) } - logURL := strings.TrimSuffix(flag.Arg(0), "/") + logURL := strings.TrimSuffix(fs.Arg(0), "/") var nextLeaf int64 = -1 for { diff --git a/cmd/ctool/main.go b/cmd/ctool/main.go new file mode 100644 index 0000000..176d1b3 --- /dev/null +++ b/cmd/ctool/main.go @@ -0,0 +1,44 @@ +// Command ctool provides tools for working with Static CT log tiles. +// +// ctool [flags] ... +// +// (C) Copyright 2026 Pim van Pelt +package main + +import ( + "fmt" + "os" +) + +const version = "0.1.0" + +func main() { + if len(os.Args) < 2 { + usage() + os.Exit(1) + } + + switch os.Args[1] { + case "fetch": + runFetch(os.Args[2:]) + case "tail": + runTail(os.Args[2:]) + default: + fmt.Fprintf(os.Stderr, "Error: unknown command %q\n\n", os.Args[1]) + usage() + os.Exit(1) + } +} + +func usage() { + fmt.Fprintf(os.Stderr, "Usage: ctool [flags] ...\n\n") + fmt.Fprintf(os.Stderr, "Commands:\n") + fmt.Fprintf(os.Stderr, " fetch fetch and decode CT log entries as JSON\n") + fmt.Fprintf(os.Stderr, " tail tail a CT log, printing one-liners for new entries\n") + fmt.Fprintf(os.Stderr, "\nRun 'ctool --help' for command-specific flags.\n") +} + +func fatal(format string, args ...any) { + fmt.Fprintf(os.Stderr, "Error: "+format+"\n", args...) + os.Exit(1) +} diff --git a/docs/ctail.md b/docs/ctail.md index bb03260..57df9f4 100644 --- a/docs/ctail.md +++ b/docs/ctail.md @@ -5,22 +5,22 @@ Tail a Static CT log, printing a one-liner per new certificate or precertificate ## Install ```bash -GOPRIVATE=git.ipng.ch go install git.ipng.ch/certificate-transparency/ctfetch/cmd/ctail@latest +GOPRIVATE=git.ipng.ch go install git.ipng.ch/certificate-transparency/ctfetch/cmd/ctool@latest ``` ## Usage ```bash -ctail [flags] +ctool tail [flags] ``` Example: ```bash -ctail https://halloumi2026h1.mon.ct.ipng.ch +ctool tail https://halloumi2026h2.mon.ct.ipng.ch ``` -By default `ctail` starts at the current tree tip and prints new entries as they appear. Use `--from-leaf 0` to replay from the beginning. +By default `ctool tail` starts at the current tree tip and prints new entries as they appear. Use `--from-leaf 0` to replay from the beginning. ## Output format diff --git a/docs/ctfetch.md b/docs/ctfetch.md index 123c34a..885383d 100644 --- a/docs/ctfetch.md +++ b/docs/ctfetch.md @@ -5,28 +5,26 @@ Fetch and decode entries from a Static CT log, outputting structured JSON. ## Install ```bash -GOPRIVATE=git.ipng.ch go install git.ipng.ch/certificate-transparency/ctfetch/cmd/ctfetch@latest +GOPRIVATE=git.ipng.ch go install git.ipng.ch/certificate-transparency/ctfetch/cmd/ctool@latest ``` -The `GOPRIVATE` variable skips the Go checksum database and module proxy, which do not index modules on `git.ipng.ch`. - ## Modes -`ctfetch` operates in two modes depending on the arguments given. +`ctool fetch` operates in two modes depending on the arguments given. ### Leaf-index mode Fetch the entry at a specific leaf index: ```bash -ctfetch [flags] [+sct] [+issuer] [+ctlog] [+all] +ctool fetch [flags] [+sct] [+issuer] [+ctlog] [+all] ``` Examples: ```bash -ctfetch https://halloumi2026h1.mon.ct.ipng.ch 629794635 -ctfetch https://halloumi2026h1.mon.ct.ipng.ch 629794635 +all +ctool fetch https://halloumi2026h1.mon.ct.ipng.ch 629794635 +ctool fetch https://halloumi2026h1.mon.ct.ipng.ch 629794635 +all ``` ### Tile-dump mode @@ -34,16 +32,16 @@ ctfetch https://halloumi2026h1.mon.ct.ipng.ch 629794635 +all Fetch all entries from a tile URL or local file. Automatically detects data tiles (log entries) and hash tiles (Merkle tree hashes). ```bash -ctfetch [flags] [+sct] [+issuer] [+ctlog] [+all] +ctool fetch [flags] [+sct] [+issuer] [+ctlog] [+all] ``` Examples: ```bash -ctfetch https://halloumi2026h1.mon.ct.ipng.ch/tile/data/x002/x460/135 -ctfetch https://halloumi2026h1.mon.ct.ipng.ch/tile/data/x002/x460/135 +sct +ctlog -ctfetch https://halloumi2026h1.mon.ct.ipng.ch/tile/0/x100/999 -ctfetch --monitoring-url https://halloumi2026h1.mon.ct.ipng.ch tile.bin +issuer +ctool fetch https://halloumi2026h1.mon.ct.ipng.ch/tile/data/x002/x460/135 +ctool fetch https://halloumi2026h1.mon.ct.ipng.ch/tile/data/x002/x460/135 +sct +ctlog +ctool fetch https://halloumi2026h1.mon.ct.ipng.ch/tile/0/x100/999 +ctool fetch --monitoring-url https://halloumi2026h1.mon.ct.ipng.ch tile.bin +issuer ``` ## Output modifiers