144 lines
3.7 KiB
Go
144 lines
3.7 KiB
Go
// Package utils provides shared functionality for dumping CT log tile entries.
|
|
// (C) Copyright 2026 Pim van Pelt <pim@ipng.ch>
|
|
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.
|
|
// Automatically detects if the tile is a data tile or hash tile.
|
|
func DumpAllEntries(tileData []byte) error {
|
|
// Try to read as data tile first
|
|
if err := dumpDataTile(tileData); 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
|
|
}
|
|
|
|
func dumpDataTile(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
|
|
}
|
|
|
|
func dumpHashTile(tileData []byte) 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)
|
|
}
|
|
|
|
numHashes := len(tileData) / hashSize
|
|
fmt.Printf("Hash tile with %d hashes:\n\n", numHashes)
|
|
|
|
for i := 0; i < numHashes; i++ {
|
|
hash := tileData[i*hashSize : (i+1)*hashSize]
|
|
fmt.Printf("Hash %d: %x\n", i, hash)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|