1 Commits

9 changed files with 51 additions and 174 deletions

View File

@@ -36,23 +36,23 @@ logs:
3. **Generate private keys:** 3. **Generate private keys:**
```bash ```bash
mkdir -p /etc/tesseract/keys mkdir -p /etc/tesseract/keys
./tesseract-genconf -c config.yaml --write gen-key ./tesseract-genconf -c config.yaml gen-key
``` ```
4. **Create directories and generate environment files:** 4. **Create directories and generate environment files:**
```bash ```bash
mkdir -p /var/lib/tesseract/example2025h1/data mkdir -p /var/lib/tesseract/example2025h1/data
./tesseract-genconf -c config.yaml --write gen-env ./tesseract-genconf -c config.yaml gen-env
``` ```
5. **Generate HTML and JSON files:** 5. **Generate HTML and JSON files:**
```bash ```bash
./tesseract-genconf -c config.yaml --write gen-html ./tesseract-genconf -c config.yaml gen-html
``` ```
6. **Generate nginx configuration files:** 6. **Generate nginx configuration files:**
```bash ```bash
./tesseract-genconf -c config.yaml --write gen-nginx ./tesseract-genconf -c config.yaml gen-nginx
``` ```
The port from the main `listen:` field will be used in the NGINX server blocks (in our case The port from the main `listen:` field will be used in the NGINX server blocks (in our case
@@ -66,25 +66,3 @@ The port from the main `listen:` field will be used in the NGINX server blocks (
# For production environment, take the ccadb 'production' roots # For production environment, take the ccadb 'production' roots
./tesseract-genconf gen-roots --source https://gouda2027h2.log.ct.ipng.ch/ --output roots-production.pem ./tesseract-genconf gen-roots --source https://gouda2027h2.log.ct.ipng.ch/ --output roots-production.pem
``` ```
### Safe File Operations with `--diff` and `--write`
The `tesseract-genconf` tool includes safety features to prevent accidental file modifications:
- **`--diff`**: Shows colored unified diffs of what would change without writing files
- **`--write`**: Required flag to actually write files to disk
- **`--no-color`**: Disables colored diff output (useful for redirecting to files)
**Recommended workflow:**
```bash
# 1. First, preview changes with --diff
./tesseract-genconf -c config.yaml --diff gen-html
# 2. Review the colored diff output, then apply changes
./tesseract-genconf -c config.yaml --write gen-html
# 3. Or combine both to see diffs and write files
./tesseract-genconf -c config.yaml --diff --write gen-html
```
**Note:** Flags must come before the command name (e.g., `--diff gen-html`, not `gen-html --diff`).

5
go.mod
View File

@@ -3,8 +3,3 @@ module cheese
go 1.24.4 go 1.24.4
require gopkg.in/yaml.v3 v3.0.1 require gopkg.in/yaml.v3 v3.0.1
require (
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
)

4
go.sum
View File

@@ -1,7 +1,3 @@
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -8,7 +8,7 @@ import (
"strings" "strings"
) )
func generateEnv(yamlFile string, wantDiff bool, allowWrite bool, useColor bool) { func generateEnv(yamlFile string) {
config := loadConfig(yamlFile) config := loadConfig(yamlFile)
// Check that all local directories exist // Check that all local directories exist
@@ -24,7 +24,7 @@ func generateEnv(yamlFile string, wantDiff bool, allowWrite bool, useColor bool)
// Create combined roots.pem file // Create combined roots.pem file
rootsPemPath := filepath.Join(logEntry.LocalDirectory, "roots.pem") rootsPemPath := filepath.Join(logEntry.LocalDirectory, "roots.pem")
err := createCombinedRootsPemWithStatus(config.Roots, logEntry.ExtraRoots, rootsPemPath, wantDiff, allowWrite, useColor) err := createCombinedRootsPemWithStatus(config.Roots, logEntry.ExtraRoots, rootsPemPath)
if err != nil { if err != nil {
log.Fatalf("Failed to create %s: %v", rootsPemPath, err) log.Fatalf("Failed to create %s: %v", rootsPemPath, err)
} }
@@ -32,7 +32,7 @@ func generateEnv(yamlFile string, wantDiff bool, allowWrite bool, useColor bool)
// Build TESSERACT_ARGS string // Build TESSERACT_ARGS string
args := []string{ args := []string{
fmt.Sprintf("--private_key=%s", logEntry.Secret), fmt.Sprintf("--private_key=%s", logEntry.Secret),
fmt.Sprintf("--origin=%s", logEntry.Origin), fmt.Sprintf("--origin=%s.%s", logEntry.ShortName, logEntry.Domain),
fmt.Sprintf("--storage_dir=%s", logEntry.LocalDirectory), fmt.Sprintf("--storage_dir=%s", logEntry.LocalDirectory),
fmt.Sprintf("--roots_pem_file=%s", rootsPemPath), fmt.Sprintf("--roots_pem_file=%s", rootsPemPath),
} }
@@ -53,14 +53,14 @@ func generateEnv(yamlFile string, wantDiff bool, allowWrite bool, useColor bool)
tesseractArgs := strings.Join(args, " ") tesseractArgs := strings.Join(args, " ")
envContent := fmt.Sprintf("TESSERACT_ARGS=\"%s\"\nOTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318\n", tesseractArgs) envContent := fmt.Sprintf("TESSERACT_ARGS=\"%s\"\nOTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318\n", tesseractArgs)
err = writeFileWithStatus(envPath, []byte(envContent), wantDiff, allowWrite, useColor) err = writeFileWithStatus(envPath, []byte(envContent))
if err != nil { if err != nil {
log.Fatalf("Failed to write %s: %v", envPath, err) log.Fatalf("Failed to write %s: %v", envPath, err)
} }
} }
} }
func createCombinedRootsPemWithStatus(rootsFile, extraRootsFile, outputPath string, wantDiff bool, allowWrite bool, useColor bool) error { func createCombinedRootsPemWithStatus(rootsFile, extraRootsFile, outputPath string) error {
// Read main roots file // Read main roots file
var combinedContent []byte var combinedContent []byte
if rootsFile != "" { if rootsFile != "" {
@@ -80,5 +80,5 @@ func createCombinedRootsPemWithStatus(rootsFile, extraRootsFile, outputPath stri
combinedContent = append(combinedContent, extraRootsData...) combinedContent = append(combinedContent, extraRootsData...)
} }
return writeFileWithStatus(outputPath, combinedContent, wantDiff, allowWrite, useColor) return writeFileWithStatus(outputPath, combinedContent)
} }

View File

@@ -74,7 +74,7 @@ const htmlTemplate = `<!DOCTYPE html>
{{range .Logs}} {{range .Logs}}
<h2>{{.Origin}}</h2> <h2>{{.ShortName}}.{{.Domain}}</h2>
<p> <p>
Log ID: <code>{{.LogID}}</code><br> Log ID: <code>{{.LogID}}</code><br>
@@ -110,7 +110,7 @@ type TemporalInterval struct {
EndExclusive string `json:"end_exclusive"` EndExclusive string `json:"end_exclusive"`
} }
func generateHTML(yamlFile string, wantDiff bool, allowWrite bool, useColor bool) { func generateHTML(yamlFile string) {
config := loadConfig(yamlFile) config := loadConfig(yamlFile)
// Check that all local directories exist // Check that all local directories exist
@@ -145,14 +145,14 @@ func generateHTML(yamlFile string, wantDiff bool, allowWrite bool, useColor bool
} }
// Write file with status // Write file with status
err = writeFileWithStatus(indexPath, buf.Bytes(), wantDiff, allowWrite, useColor) err = writeFileWithStatus(indexPath, buf.Bytes())
if err != nil { if err != nil {
log.Fatalf("Failed to write HTML to %s: %v", indexPath, err) log.Fatalf("Failed to write HTML to %s: %v", indexPath, err)
} }
// Generate log.v3.json for this log // Generate log.v3.json for this log
jsonPath := filepath.Join(logEntry.LocalDirectory, "log.v3.json") jsonPath := filepath.Join(logEntry.LocalDirectory, "log.v3.json")
err = generateLogJSONWithStatus(logEntry, jsonPath, wantDiff, allowWrite, useColor) err = generateLogJSONWithStatus(logEntry, jsonPath)
if err != nil { if err != nil {
log.Fatalf("Failed to generate %s: %v", jsonPath, err) log.Fatalf("Failed to generate %s: %v", jsonPath, err)
} }
@@ -209,9 +209,9 @@ func computeKeyInfo(logEntry *Log) error {
return nil return nil
} }
func generateLogJSONWithStatus(logEntry Log, outputPath string, wantDiff bool, allowWrite bool, useColor bool) error { func generateLogJSONWithStatus(logEntry Log, outputPath string) error {
logJSON := LogV3JSON{ logJSON := LogV3JSON{
Description: logEntry.Origin, Description: fmt.Sprintf("%s.%s", logEntry.ShortName, logEntry.Domain),
SubmissionURL: fmt.Sprintf("%s/", logEntry.SubmissionPrefix), SubmissionURL: fmt.Sprintf("%s/", logEntry.SubmissionPrefix),
MonitoringURL: fmt.Sprintf("%s/", logEntry.MonitoringPrefix), MonitoringURL: fmt.Sprintf("%s/", logEntry.MonitoringPrefix),
TemporalInterval: TemporalInterval{ TemporalInterval: TemporalInterval{
@@ -228,5 +228,5 @@ func generateLogJSONWithStatus(logEntry Log, outputPath string, wantDiff bool, a
return fmt.Errorf("failed to marshal JSON: %v", err) return fmt.Errorf("failed to marshal JSON: %v", err)
} }
return writeFileWithStatus(outputPath, jsonData, wantDiff, allowWrite, useColor) return writeFileWithStatus(outputPath, jsonData)
} }

View File

@@ -12,12 +12,7 @@ import (
"path/filepath" "path/filepath"
) )
func generateKeys(yamlFile string, wantDiff bool, allowWrite bool, useColor bool) { func generateKeys(yamlFile string) {
if !allowWrite {
fmt.Printf("Key generation requires --write flag\n")
return
}
config := loadConfig(yamlFile) config := loadConfig(yamlFile)
// Generate keys for each log // Generate keys for each log

View File

@@ -5,12 +5,8 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"strings"
"time" "time"
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -23,6 +19,7 @@ type Config struct {
type Log struct { type Log struct {
ShortName string `yaml:"shortname"` ShortName string `yaml:"shortname"`
Domain string `yaml:"domain"`
Inception string `yaml:"inception"` Inception string `yaml:"inception"`
Period int `yaml:"period"` Period int `yaml:"period"`
PoolSize int `yaml:"poolsize"` PoolSize int `yaml:"poolsize"`
@@ -41,14 +38,10 @@ type Log struct {
PublicKeyPEM string PublicKeyPEM string
PublicKeyDERB64 string PublicKeyDERB64 string
PublicKeyBase64 string PublicKeyBase64 string
Origin string
} }
func main() { func main() {
configFile := flag.String("c", "./tesseract-staging.yaml", "Path to the YAML configuration file") configFile := flag.String("c", "./tesseract-staging.yaml", "Path to the YAML configuration file")
wantDiff := flag.Bool("diff", false, "Show unified diff of changes")
allowWrite := flag.Bool("write", false, "Allow writing files (required for actual file modifications)")
noColor := flag.Bool("no-color", false, "Disable colored diff output")
flag.Parse() flag.Parse()
args := flag.Args() args := flag.Args()
@@ -59,15 +52,15 @@ func main() {
switch args[0] { switch args[0] {
case "gen-html": case "gen-html":
generateHTML(*configFile, *wantDiff, *allowWrite, !*noColor) generateHTML(*configFile)
case "gen-env": case "gen-env":
generateEnv(*configFile, *wantDiff, *allowWrite, !*noColor) generateEnv(*configFile)
case "gen-key": case "gen-key":
generateKeys(*configFile, *wantDiff, *allowWrite, !*noColor) generateKeys(*configFile)
case "gen-nginx": case "gen-nginx":
generateNginx(*configFile, *wantDiff, *allowWrite, !*noColor) generateNginx(*configFile)
case "gen-roots": case "gen-roots":
generateRoots(args[1:], *wantDiff, *allowWrite, !*noColor) generateRoots(args[1:])
default: default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", args[0]) fmt.Fprintf(os.Stderr, "Unknown command: %s\n", args[0])
showHelp() showHelp()
@@ -92,116 +85,41 @@ func loadConfig(yamlFile string) Config {
config.Listen = []string{":8080"} config.Listen = []string{":8080"}
} }
// Set defaults for log entries // Set defaults for log entries and check for empty/missing values
for i := range config.Logs { for i := range config.Logs {
if config.Logs[i].PoolSize == 0 { // Checks are in order of fields of the Log struct
config.Logs[i].PoolSize = 750
if config.Logs[i].ShortName == "" {
log.Fatalf("Log %d is missing a ShortName", i)
} }
if config.Logs[i].Domain == "" {
log.Fatalf("Log %d (%s) is missing a value for Domain", i, config.Logs[i].ShortName)
}
if config.Logs[i].Period == 0 { if config.Logs[i].Period == 0 {
config.Logs[i].Period = 200 config.Logs[i].Period = 200
} }
// Extract hostname from SubmissionPrefix to set Origin if config.Logs[i].PoolSize == 0 {
if config.Logs[i].SubmissionPrefix != "" { config.Logs[i].PoolSize = 750
hostname, err := extractHostname(config.Logs[i].SubmissionPrefix)
if err != nil {
log.Fatalf("Failed to parse SubmissionPrefix URL for %s: %v", config.Logs[i].ShortName, err)
}
config.Logs[i].Origin = hostname
} }
} }
return config return config
} }
// ANSI color codes func writeFileWithStatus(filename string, content []byte) error {
const (
colorReset = "\033[0m"
colorRed = "\033[31m"
colorGreen = "\033[32m"
colorYellow = "\033[33m"
colorCyan = "\033[36m"
)
// colorizeUnifiedDiff adds ANSI color codes to unified diff output
func colorizeUnifiedDiff(diff string) string {
lines := strings.Split(diff, "\n")
var colorizedLines []string
for _, line := range lines {
switch {
case strings.HasPrefix(line, "---"):
// File deletion header in cyan
colorizedLines = append(colorizedLines, colorCyan+line+colorReset)
case strings.HasPrefix(line, "+++"):
// File addition header in cyan
colorizedLines = append(colorizedLines, colorCyan+line+colorReset)
case strings.HasPrefix(line, "@@"):
// Hunk header in yellow
colorizedLines = append(colorizedLines, colorYellow+line+colorReset)
case strings.HasPrefix(line, "-"):
// Deleted lines in red
colorizedLines = append(colorizedLines, colorRed+line+colorReset)
case strings.HasPrefix(line, "+"):
// Added lines in green
colorizedLines = append(colorizedLines, colorGreen+line+colorReset)
default:
// Context lines unchanged
colorizedLines = append(colorizedLines, line)
}
}
return strings.Join(colorizedLines, "\n")
}
func writeFileWithStatus(filename string, content []byte, wantDiff bool, allowWrite bool, useColor bool) error {
existingContent, err := os.ReadFile(filename) existingContent, err := os.ReadFile(filename)
isNew := os.IsNotExist(err) if os.IsNotExist(err) {
isUnchanged := false fmt.Printf("Creating %s\n", filename)
if isNew {
if allowWrite {
fmt.Printf("Creating %s\n", filename)
} else {
fmt.Printf("Would create %s\n", filename)
}
} else if err != nil { } else if err != nil {
return fmt.Errorf("failed to read existing file %s: %v", filename, err) return fmt.Errorf("failed to read existing file %s: %v", filename, err)
} else if string(existingContent) == string(content) { } else if string(existingContent) == string(content) {
fmt.Printf("Unchanged %s\n", filename) fmt.Printf("Unchanged %s\n", filename)
isUnchanged = true
} else {
if allowWrite {
fmt.Printf("Updating %s\n", filename)
} else {
fmt.Printf("Would update %s\n", filename)
}
}
if wantDiff && !isUnchanged {
if isNew {
// For new files, show the entire content as added
edits := myers.ComputeEdits(span.URIFromPath(filename), "", string(content))
diff := fmt.Sprint(gotextdiff.ToUnified("/dev/null", filename, "", edits))
if useColor {
fmt.Print(colorizeUnifiedDiff(diff))
} else {
fmt.Print(diff)
}
} else {
// For existing files, show the diff
edits := myers.ComputeEdits(span.URIFromPath(filename), string(existingContent), string(content))
diff := fmt.Sprint(gotextdiff.ToUnified(filename, filename+".new", string(existingContent), edits))
if useColor {
fmt.Print(colorizeUnifiedDiff(diff))
} else {
fmt.Print(diff)
}
}
}
if isUnchanged || !allowWrite {
return nil return nil
} else {
fmt.Printf("Updating %s\n", filename)
} }
err = os.WriteFile(filename, content, 0644) err = os.WriteFile(filename, content, 0644)
@@ -213,17 +131,8 @@ func writeFileWithStatus(filename string, content []byte, wantDiff bool, allowWr
func showHelp() { func showHelp() {
fmt.Printf("Usage: %s [options] <command>\n\n", os.Args[0]) fmt.Printf("Usage: %s [options] <command>\n\n", os.Args[0])
fmt.Printf("Note: Flags must come before the command name.\n\n")
fmt.Printf("Options:\n") fmt.Printf("Options:\n")
fmt.Printf(" -c <file> Path to YAML configuration file (default: ./tesseract-staging.yaml)\n") fmt.Printf(" -c <file> Path to YAML configuration file (default: ./tesseract-staging.yaml)\n\n")
fmt.Printf(" --diff Show unified diff of changes without writing files\n")
fmt.Printf(" --write Allow writing files (required for actual file modifications)\n")
fmt.Printf(" --no-color Disable colored diff output\n\n")
fmt.Printf("Examples:\n")
fmt.Printf(" %s --diff gen-html # Show colored diffs without writing\n", os.Args[0])
fmt.Printf(" %s --diff --no-color gen-html # Show plain diffs without writing\n", os.Args[0])
fmt.Printf(" %s --write gen-html # Write files\n", os.Args[0])
fmt.Printf(" %s --diff --write gen-html # Show colored diffs and write files\n\n", os.Args[0])
fmt.Printf("Commands:\n") fmt.Printf("Commands:\n")
fmt.Printf(" gen-html Generate index.html and log.v3.json files in each log's localdirectory.\n") fmt.Printf(" gen-html Generate index.html and log.v3.json files in each log's localdirectory.\n")
fmt.Printf(" Creates HTML pages with log information and CT log metadata JSON.\n") fmt.Printf(" Creates HTML pages with log information and CT log metadata JSON.\n")

View File

@@ -76,7 +76,7 @@ type NginxTemplateData struct {
ListenPort string ListenPort string
} }
func generateNginx(yamlFile string, wantDiff bool, allowWrite bool, useColor bool) { func generateNginx(yamlFile string) {
config := loadConfig(yamlFile) config := loadConfig(yamlFile)
// Extract port from first listen address // Extract port from first listen address
@@ -123,7 +123,7 @@ func generateNginx(yamlFile string, wantDiff bool, allowWrite bool, useColor boo
} }
// Write file with status // Write file with status
err = writeFileWithStatus(outputPath, buf.Bytes(), wantDiff, allowWrite, useColor) err = writeFileWithStatus(outputPath, buf.Bytes())
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write nginx config file %s: %v\n", outputPath, err) fmt.Fprintf(os.Stderr, "Failed to write nginx config file %s: %v\n", outputPath, err)
continue continue
@@ -151,10 +151,14 @@ func extractPort(listenAddr string) string {
} }
func extractHostname(urlStr string) (string, error) { func extractHostname(urlStr string) (string, error) {
if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") {
urlStr = "https://" + urlStr
}
parsedURL, err := url.Parse(urlStr) parsedURL, err := url.Parse(urlStr)
if err != nil { if err != nil {
return "", err return "", err
} }
return parsedURL.Host, nil return parsedURL.Hostname(), nil
} }

View File

@@ -16,7 +16,7 @@ type CTLogRootsResponse struct {
Certificates []string `json:"certificates"` Certificates []string `json:"certificates"`
} }
func generateRoots(args []string, wantDiff bool, allowWrite bool, useColor bool) { func generateRoots(args []string) {
sourceURL := "https://rennet2027h2.log.ct.ipng.ch/" sourceURL := "https://rennet2027h2.log.ct.ipng.ch/"
outputFile := "roots.pem" outputFile := "roots.pem"
@@ -107,7 +107,7 @@ func generateRoots(args []string, wantDiff bool, allowWrite bool, useColor bool)
} }
// Write all certificates to file with status // Write all certificates to file with status
err = writeFileWithStatus(outputFile, pemBuffer.Bytes(), wantDiff, allowWrite, useColor) err = writeFileWithStatus(outputFile, pemBuffer.Bytes())
if err != nil { if err != nil {
log.Fatalf("Failed to write output file %s: %v", outputFile, err) log.Fatalf("Failed to write output file %s: %v", outputFile, err)
} }