Output JSON

This commit is contained in:
2026-01-12 22:48:15 +01:00
parent dbbae65e45
commit 66835aab9d
3 changed files with 104 additions and 36 deletions

View File

@@ -4,6 +4,7 @@
package main package main
import ( import (
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"os" "os"
@@ -90,17 +91,30 @@ func main() {
if *dumpAll { if *dumpAll {
// Dump all entries in the tile // Dump all entries in the tile
if err := utils.DumpAllEntries(tileData); err != nil { result, err := utils.DumpAllEntries(tileData)
if err != nil {
fatal("%v", err) fatal("%v", err)
} }
printJSON(result)
} else { } else {
// Dump only the specific entry at the position // 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) 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) { func fatal(format string, args ...any) {
fmt.Fprintf(os.Stderr, "Error: "+format+"\n", args...) fmt.Fprintf(os.Stderr, "Error: "+format+"\n", args...)
os.Exit(1) os.Exit(1)

View File

@@ -3,6 +3,7 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@@ -63,9 +64,20 @@ func main() {
fmt.Fprintf(os.Stderr, "Tile size: %d bytes\n\n", len(tileData)) fmt.Fprintf(os.Stderr, "Tile size: %d bytes\n\n", len(tileData))
// Dump all entries // Dump all entries
if err := utils.DumpAllEntries(tileData); err != nil { result, err := utils.DumpAllEntries(tileData)
if err != nil {
fatal("%v", err) 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) { func fatal(format string, args ...any) {

View File

@@ -5,6 +5,7 @@ package utils
import ( import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -16,6 +17,32 @@ import (
const maxCompressRatio = 100 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. // FetchURL fetches data from a URL.
func FetchURL(url string) ([]byte, error) { func FetchURL(url string) ([]byte, error) {
resp, err := http.Get(url) resp, err := http.Get(url)
@@ -42,61 +69,70 @@ func Decompress(data []byte) ([]byte, error) {
return io.ReadAll(io.LimitReader(r, maxSize)) 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. // 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 // 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 // If it fails, try as hash tile
fmt.Fprintf(os.Stderr, "Not a data tile, trying as hash tile...\n") fmt.Fprintf(os.Stderr, "Not a data tile, trying as hash tile...\n")
return dumpHashTile(tileData) return dumpHashTile(tileData)
} }
return nil return result, nil
} }
func dumpDataTile(tileData []byte) error { func dumpDataTile(tileData []byte) (*DumpResult, error) {
entryNum := 0 entryNum := 0
var entries []Entry
for len(tileData) > 0 { for len(tileData) > 0 {
e, remaining, err := sunlight.ReadTileLeaf(tileData) e, remaining, err := sunlight.ReadTileLeaf(tileData)
if err != nil { 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 tileData = remaining
dumpEntry(e, entryNum) entry := convertEntry(e, entryNum)
fmt.Println() entries = append(entries, entry)
entryNum++ entryNum++
} }
fmt.Printf("Total entries: %d\n", entryNum) return &DumpResult{
return nil Entries: entries,
TotalEntries: entryNum,
}, nil
} }
func dumpHashTile(tileData []byte) error { func dumpHashTile(tileData []byte) (*DumpResult, error) {
const hashSize = 32 // SHA-256 hash size const hashSize = 32 // SHA-256 hash size
if len(tileData)%hashSize != 0 { 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 numHashes := len(tileData) / hashSize
fmt.Printf("Hash tile with %d hashes:\n\n", numHashes) hashes := make([]string, numHashes)
for i := 0; i < numHashes; i++ { for i := 0; i < numHashes; i++ {
hash := tileData[i*hashSize : (i+1)*hashSize] 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. // DumpEntryAtPosition reads and returns a specific entry at the given position.
func DumpEntryAtPosition(tileData []byte, position int, expectedIndex int64) error { func DumpEntryAtPosition(tileData []byte, position int, expectedIndex int64) (*Entry, error) {
entryNum := 0 entryNum := 0
for len(tileData) > 0 { for len(tileData) > 0 {
e, remaining, err := sunlight.ReadTileLeaf(tileData) e, remaining, err := sunlight.ReadTileLeaf(tileData)
if err != nil { 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 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", fmt.Fprintf(os.Stderr, "WARNING: Expected leaf index %d but found %d at position %d\n",
expectedIndex, e.LeafIndex, position) expectedIndex, e.LeafIndex, position)
} }
dumpEntry(e, entryNum) entry := convertEntry(e, entryNum)
return nil return &entry, nil
} }
entryNum++ 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) { func convertEntry(e *sunlight.LogEntry, entryNum int) Entry {
fmt.Printf("=== Entry %d ===\n", entryNum) entry := Entry{
fmt.Printf("Leaf Index: %d\n", e.LeafIndex) EntryNumber: entryNum,
fmt.Printf("Timestamp: %d\n", e.Timestamp) LeafIndex: e.LeafIndex,
fmt.Printf("Is Precert: %v\n", e.IsPrecert) Timestamp: e.Timestamp,
IsPrecert: e.IsPrecert,
CertificateSize: len(e.Certificate),
}
if e.IsPrecert { 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 { 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 { 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 // Try to extract parsed certificate info
if trimmed, err := e.TrimmedEntry(); err == nil { if trimmed, err := e.TrimmedEntry(); err == nil {
if data, err := json.MarshalIndent(trimmed, " ", " "); err == nil { if data, err := json.Marshal(trimmed); err == nil {
fmt.Printf("Parsed Certificate Info:\n %s\n", data) entry.ParsedCertInfo = data
} }
} }
return entry
} }