// Package utils provides shared functionality for dumping CT log tile entries. // (C) Copyright 2026 Pim van Pelt package utils import ( "bytes" "compress/gzip" "encoding/json" "fmt" "io" "net/http" "os" "filippo.io/sunlight" ) const maxCompressRatio = 100 // FetchURL fetches data from a URL. func FetchURL(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("HTTP %d", resp.StatusCode) } return io.ReadAll(resp.Body) } // Decompress decompresses gzip-compressed data, or returns the data as-is if not compressed. func Decompress(data []byte) ([]byte, error) { r, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { // Not gzipped, return as-is return data, nil } maxSize := int64(len(data)) * maxCompressRatio return io.ReadAll(io.LimitReader(r, maxSize)) } // DumpAllEntries reads and dumps all entries from tile data. func DumpAllEntries(tileData []byte) error { entryNum := 0 for len(tileData) > 0 { e, remaining, err := sunlight.ReadTileLeaf(tileData) if err != nil { return fmt.Errorf("failed to read entry %d: %w", entryNum, err) } tileData = remaining dumpEntry(e, entryNum) fmt.Println() entryNum++ } fmt.Printf("Total entries: %d\n", entryNum) return nil } // DumpEntryAtPosition reads and dumps a specific entry at the given position. func DumpEntryAtPosition(tileData []byte, position int, expectedIndex int64) error { entryNum := 0 for len(tileData) > 0 { e, remaining, err := sunlight.ReadTileLeaf(tileData) if err != nil { return fmt.Errorf("failed to read entry %d: %w", entryNum, err) } tileData = remaining if entryNum == position { if e.LeafIndex != expectedIndex { fmt.Fprintf(os.Stderr, "WARNING: Expected leaf index %d but found %d at position %d\n", expectedIndex, e.LeafIndex, position) } dumpEntry(e, entryNum) return nil } entryNum++ } return fmt.Errorf("position %d not found in tile (only %d entries)", position, entryNum) } func dumpEntry(e *sunlight.LogEntry, entryNum int) { fmt.Printf("=== Entry %d ===\n", entryNum) fmt.Printf("Leaf Index: %d\n", e.LeafIndex) fmt.Printf("Timestamp: %d\n", e.Timestamp) fmt.Printf("Is Precert: %v\n", e.IsPrecert) if e.IsPrecert { fmt.Printf("Issuer Key Hash: %x\n", e.IssuerKeyHash) } fmt.Printf("Certificate: %d bytes\n", len(e.Certificate)) if e.PreCertificate != nil { fmt.Printf("PreCertificate: %d bytes\n", len(e.PreCertificate)) } fmt.Printf("Chain Fingerprints: %d entries\n", len(e.ChainFingerprints)) for i, fp := range e.ChainFingerprints { fmt.Printf(" [%d]: %x\n", i, fp) } // Try to extract parsed certificate info if trimmed, err := e.TrimmedEntry(); err == nil { if data, err := json.MarshalIndent(trimmed, " ", " "); err == nil { fmt.Printf("Parsed Certificate Info:\n %s\n", data) } } }