From 66835aab9de89a0ae7f4deb66068d48009430f20 Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Mon, 12 Jan 2026 22:48:15 +0100 Subject: [PATCH] Output JSON --- cmd/ctfetch/main.go | 18 ++++++- cmd/tiledump/main.go | 14 +++++- internal/utils/utils.go | 108 ++++++++++++++++++++++++++++------------ 3 files changed, 104 insertions(+), 36 deletions(-) diff --git a/cmd/ctfetch/main.go b/cmd/ctfetch/main.go index 4a9a624..cf1c294 100644 --- a/cmd/ctfetch/main.go +++ b/cmd/ctfetch/main.go @@ -4,6 +4,7 @@ package main import ( + "encoding/json" "flag" "fmt" "os" @@ -90,17 +91,30 @@ func main() { if *dumpAll { // Dump all entries in the tile - if err := utils.DumpAllEntries(tileData); err != nil { + result, err := utils.DumpAllEntries(tileData) + if err != nil { fatal("%v", err) } + printJSON(result) } else { // Dump only the specific entry at the position - if err := utils.DumpEntryAtPosition(tileData, int(positionInTile), leafIndex); err != nil { + entry, err := utils.DumpEntryAtPosition(tileData, int(positionInTile), leafIndex) + if err != nil { fatal("%v", err) } + printJSON(entry) } } +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) diff --git a/cmd/tiledump/main.go b/cmd/tiledump/main.go index d6e53fc..2081dc5 100644 --- a/cmd/tiledump/main.go +++ b/cmd/tiledump/main.go @@ -3,6 +3,7 @@ package main import ( + "encoding/json" "fmt" "os" "strings" @@ -63,9 +64,20 @@ func main() { fmt.Fprintf(os.Stderr, "Tile size: %d bytes\n\n", len(tileData)) // Dump all entries - if err := utils.DumpAllEntries(tileData); err != nil { + result, err := utils.DumpAllEntries(tileData) + 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) { diff --git a/internal/utils/utils.go b/internal/utils/utils.go index c3eda34..c7a5830 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -5,6 +5,7 @@ package utils import ( "bytes" "compress/gzip" + "encoding/hex" "encoding/json" "fmt" "io" @@ -16,6 +17,32 @@ import ( const maxCompressRatio = 100 +// Entry represents a CT log entry in JSON format. +type Entry struct { + EntryNumber int `json:"entry_number"` + LeafIndex int64 `json:"leaf_index"` + Timestamp int64 `json:"timestamp"` + IsPrecert bool `json:"is_precert"` + IssuerKeyHash string `json:"issuer_key_hash,omitempty"` + CertificateSize int `json:"certificate_size"` + PreCertificateSize *int `json:"precertificate_size,omitempty"` + ChainFingerprints []string `json:"chain_fingerprints"` + ParsedCertInfo json.RawMessage `json:"parsed_cert_info,omitempty"` +} + +// HashTileOutput represents a hash tile in JSON format. +type HashTileOutput struct { + NumHashes int `json:"num_hashes"` + Hashes []string `json:"hashes"` +} + +// DumpResult is the result of dumping entries or hashes from a tile. +type DumpResult struct { + Entries []Entry `json:"entries,omitempty"` + HashTile *HashTileOutput `json:"hash_tile,omitempty"` + TotalEntries int `json:"total_entries,omitempty"` +} + // FetchURL fetches data from a URL. func FetchURL(url string) ([]byte, error) { resp, err := http.Get(url) @@ -42,61 +69,70 @@ func Decompress(data []byte) ([]byte, error) { return io.ReadAll(io.LimitReader(r, maxSize)) } -// DumpAllEntries reads and dumps all entries from tile data. +// DumpAllEntries reads and returns all entries from tile data as JSON-serializable structures. // Automatically detects if the tile is a data tile or hash tile. -func DumpAllEntries(tileData []byte) error { +func DumpAllEntries(tileData []byte) (*DumpResult, error) { // Try to read as data tile first - if err := dumpDataTile(tileData); err != nil { + result, err := dumpDataTile(tileData) + if err != nil { // If it fails, try as hash tile fmt.Fprintf(os.Stderr, "Not a data tile, trying as hash tile...\n") return dumpHashTile(tileData) } - return nil + return result, nil } -func dumpDataTile(tileData []byte) error { +func dumpDataTile(tileData []byte) (*DumpResult, error) { entryNum := 0 + var entries []Entry for len(tileData) > 0 { e, remaining, err := sunlight.ReadTileLeaf(tileData) if err != nil { - return fmt.Errorf("failed to read entry %d: %w", entryNum, err) + return nil, fmt.Errorf("failed to read entry %d: %w", entryNum, err) } tileData = remaining - dumpEntry(e, entryNum) - fmt.Println() + entry := convertEntry(e, entryNum) + entries = append(entries, entry) entryNum++ } - fmt.Printf("Total entries: %d\n", entryNum) - return nil + return &DumpResult{ + Entries: entries, + TotalEntries: entryNum, + }, nil } -func dumpHashTile(tileData []byte) error { +func dumpHashTile(tileData []byte) (*DumpResult, error) { const hashSize = 32 // SHA-256 hash size if len(tileData)%hashSize != 0 { - return fmt.Errorf("invalid hash tile: size %d is not a multiple of %d", len(tileData), hashSize) + return nil, fmt.Errorf("invalid hash tile: size %d is not a multiple of %d", len(tileData), hashSize) } numHashes := len(tileData) / hashSize - fmt.Printf("Hash tile with %d hashes:\n\n", numHashes) + hashes := make([]string, numHashes) for i := 0; i < numHashes; i++ { hash := tileData[i*hashSize : (i+1)*hashSize] - fmt.Printf("Hash %d: %x\n", i, hash) + hashes[i] = hex.EncodeToString(hash) } - return nil + return &DumpResult{ + HashTile: &HashTileOutput{ + NumHashes: numHashes, + Hashes: hashes, + }, + }, nil } -// DumpEntryAtPosition reads and dumps a specific entry at the given position. -func DumpEntryAtPosition(tileData []byte, position int, expectedIndex int64) error { +// DumpEntryAtPosition reads and returns a specific entry at the given position. +func DumpEntryAtPosition(tileData []byte, position int, expectedIndex int64) (*Entry, 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) + return nil, fmt.Errorf("failed to read entry %d: %w", entryNum, err) } tileData = remaining @@ -105,39 +141,45 @@ func DumpEntryAtPosition(tileData []byte, position int, expectedIndex int64) err 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 + entry := convertEntry(e, entryNum) + return &entry, nil } entryNum++ } - return fmt.Errorf("position %d not found in tile (only %d entries)", position, entryNum) + return nil, 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) +func convertEntry(e *sunlight.LogEntry, entryNum int) Entry { + entry := Entry{ + EntryNumber: entryNum, + LeafIndex: e.LeafIndex, + Timestamp: e.Timestamp, + IsPrecert: e.IsPrecert, + CertificateSize: len(e.Certificate), + } if e.IsPrecert { - fmt.Printf("Issuer Key Hash: %x\n", e.IssuerKeyHash) + entry.IssuerKeyHash = hex.EncodeToString(e.IssuerKeyHash[:]) } - fmt.Printf("Certificate: %d bytes\n", len(e.Certificate)) if e.PreCertificate != nil { - fmt.Printf("PreCertificate: %d bytes\n", len(e.PreCertificate)) + size := len(e.PreCertificate) + entry.PreCertificateSize = &size } - fmt.Printf("Chain Fingerprints: %d entries\n", len(e.ChainFingerprints)) + // Convert chain fingerprints to hex strings + entry.ChainFingerprints = make([]string, len(e.ChainFingerprints)) for i, fp := range e.ChainFingerprints { - fmt.Printf(" [%d]: %x\n", i, fp) + entry.ChainFingerprints[i] = hex.EncodeToString(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) + if data, err := json.Marshal(trimmed); err == nil { + entry.ParsedCertInfo = data } } + + return entry }