package main import ( "flag" "fmt" "log" "os" "time" "gopkg.in/yaml.v3" ) type Config struct { Listen []string `yaml:"listen"` Checkpoints string `yaml:"checkpoints"` Roots string `yaml:"roots"` Logs []Log `yaml:"logs"` } type Log struct { ShortName string `yaml:"shortname"` Domain string `yaml:"domain"` Inception string `yaml:"inception"` Period int `yaml:"period"` PoolSize int `yaml:"poolsize"` SubmissionPrefix string `yaml:"submissionprefix"` MonitoringPrefix string `yaml:"monitoringprefix"` CCadbRoots string `yaml:"ccadbroots"` ExtraRoots string `yaml:"extraroots"` Secret string `yaml:"secret"` Cache string `yaml:"cache"` LocalDirectory string `yaml:"localdirectory"` Listen string `yaml:"listen"` NotAfterStart time.Time `yaml:"notafterstart"` NotAfterLimit time.Time `yaml:"notafterlimit"` // Computed fields LogID string PublicKeyPEM string PublicKeyDERB64 string PublicKeyBase64 string } func main() { configFile := flag.String("c", "./tesseract-staging.yaml", "Path to the YAML configuration file") flag.Parse() args := flag.Args() if len(args) == 0 { showHelp() return } switch args[0] { case "gen-html": generateHTML(*configFile) case "gen-env": generateEnv(*configFile) case "gen-key": generateKeys(*configFile) case "gen-nginx": generateNginx(*configFile) case "gen-roots": generateRoots(args[1:]) default: fmt.Fprintf(os.Stderr, "Unknown command: %s\n", args[0]) showHelp() os.Exit(1) } } func loadConfig(yamlFile string) Config { data, err := os.ReadFile(yamlFile) if err != nil { log.Fatalf("Failed to read YAML file: %v", err) } var config Config err = yaml.Unmarshal(data, &config) if err != nil { log.Fatalf("Failed to parse YAML: %v", err) } // Set default listen port if not configured if len(config.Listen) == 0 { config.Listen = []string{":8080"} } // Checkpoints & Roots are not used in-code, not checking for being set/valid // Ensure there are logs configured if len(config.Logs) == 0 { log.Fatalf("Parsed YAML did not include any 'logs'") } // Set defaults for log entries and check for empty/missing values for i := range config.Logs { // Checks are in order of fields of the Log struct 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) } // Inception is not used in-code if config.Logs[i].Period == 0 { config.Logs[i].Period = 200 } if config.Logs[i].PoolSize == 0 { config.Logs[i].PoolSize = 750 } if config.Logs[i].SubmissionPrefix == "" { log.Fatalf("Log %d (%s) is missing a value for SubmissionPrefix", i, config.Logs[i].ShortName) } if config.Logs[i].MonitoringPrefix == "" { log.Fatalf("Log %d (%s) is missing a value for MonitoringPrefix", i, config.Logs[i].ShortName) } // CCadbRoots is not used in-code // ExtraRoots is optional if config.Logs[i].Secret == "" { log.Fatalf("Log %d (%s) is missing a value for Secret", i, config.Logs[i].ShortName) } // Cache is not used in-code if config.Logs[i].LocalDirectory == "" { log.Fatalf("Log %d (%s) is missing a value for LocalDirectory", i, config.Logs[i].ShortName) } // Listen, NotAfterStart and NotAfterLimit are optional // These fields are exported due to HTML templates // but should not be provided/filled by the user if config.Logs[i].LogID != "" { log.Fatalf("Log %d (%s) has field LogID should not be configured (%s)", i, config.Logs[i].ShortName, config.Logs[i].LogID) } if config.Logs[i].PublicKeyPEM != "" { log.Fatalf("Log %d (%s) has field PublicKeyPEM should not be configured (%s)", i, config.Logs[i].ShortName, config.Logs[i].PublicKeyPEM) } if config.Logs[i].PublicKeyDERB64 != "" { log.Fatalf("Log %d (%s) has field PublicKeyDERB64 should not be configured (%s)", i, config.Logs[i].ShortName, config.Logs[i].PublicKeyDERB64) } if config.Logs[i].PublicKeyBase64 != "" { log.Fatalf("Log %d (%s) has field PublicKeyBase64 should not be configured (%s)", i, config.Logs[i].ShortName, config.Logs[i].PublicKeyBase64) } } return config } func writeFileWithStatus(filename string, content []byte) error { existingContent, err := os.ReadFile(filename) if os.IsNotExist(err) { fmt.Printf("Creating %s\n", filename) } else if err != nil { return fmt.Errorf("failed to read existing file %s: %v", filename, err) } else if string(existingContent) == string(content) { fmt.Printf("Unchanged %s\n", filename) return nil } else { fmt.Printf("Updating %s\n", filename) } err = os.WriteFile(filename, content, 0644) if err != nil { return fmt.Errorf("failed to write file %s: %v", filename, err) } return nil } func showHelp() { fmt.Printf("Usage: %s [options] \n\n", os.Args[0]) fmt.Printf("Options:\n") fmt.Printf(" -c Path to YAML configuration file (default: ./tesseract-staging.yaml)\n\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(" Creates HTML pages with log information and CT log metadata JSON.\n") fmt.Printf(" Computes LOG_ID and public keys from private keys.\n\n") fmt.Printf(" gen-env Generate .env files and combined roots.pem in each log's localdirectory.\n") fmt.Printf(" Creates TESSERACT_ARGS environment variable with command line flags.\n") fmt.Printf(" Combines global roots and log-specific extraroots into roots.pem.\n\n") fmt.Printf(" gen-key Generate prime256v1 private keys for each log (only if they don't exist).\n") fmt.Printf(" Creates EC private key files at the path specified in log.secret.\n\n") fmt.Printf(" gen-nginx Generate nginx configuration files for each log's monitoring endpoint.\n") fmt.Printf(" Creates nginx-.conf files in each log's localdirectory.\n\n") fmt.Printf(" gen-roots Download root certificates from a Certificate Transparency log.\n") fmt.Printf(" Options: --source (default: https://rennet2027h2.log.ct.ipng.ch/)\n") fmt.Printf(" --output (default: roots.pem)\n\n") }