package main import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "flag" "fmt" "io" "log" "net/http" "os" "path/filepath" "strings" "text/template" "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"` 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"` NotAfterStart time.Time `yaml:"notafterstart"` NotAfterLimit time.Time `yaml:"notafterlimit"` // Computed fields LogID string PublicKeyPEM string PublicKeyDERB64 string PublicKeyBase64 string } const htmlTemplate = ` TesseraCT

A TesseraCT logo, a stylized four-dimensional hypercube with on the left side penstrokes of orange with a padlock, and on the right penstrokes of dark blue with a magnifying glass.

This is a TesseraCT Certificate Transparency log instance.


The following logs are active. {{range .Logs}}

{{.ShortName}}.log.ct.ipng.ch

Log ID: {{.LogID}}
Monitoring prefix: {{.MonitoringPrefix}}/
Submission prefix: {{.SubmissionPrefix}}/
Interval: {{.NotAfterStart.Format "2006-01-02T15:04:05Z"}} – {{.NotAfterLimit.Format "2006-01-02T15:04:05Z"}}
Links: checkpoint key get-roots json
Ratelimit: {{.PoolSize}} req/s

{{.PublicKeyPEM}}
{{end}} ` 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-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 defaults for log entries for i := range config.Logs { if config.Logs[i].PoolSize == 0 { config.Logs[i].PoolSize = 750 } if config.Logs[i].Period == 0 { config.Logs[i].Period = 200 } } return config } 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-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") } func showConfig(yamlFile string) { config := loadConfig(yamlFile) fmt.Printf("Config loaded successfully:\n") fmt.Printf("Listen addresses: %v\n", config.Listen) fmt.Printf("Checkpoints: %s\n", config.Checkpoints) fmt.Printf("Number of logs: %d\n", len(config.Logs)) for i, logEntry := range config.Logs { fmt.Printf("Log %d: %s (Period: %d, Pool size: %d)\n", i+1, logEntry.ShortName, logEntry.Period, logEntry.PoolSize) } } func generateHTML(yamlFile string) { config := loadConfig(yamlFile) // Check that all local directories exist for _, logEntry := range config.Logs { if _, err := os.Stat(logEntry.LocalDirectory); os.IsNotExist(err) { log.Fatalf("User is required to create %s", logEntry.LocalDirectory) } } // Compute key information for each log for i := range config.Logs { err := computeKeyInfo(&config.Logs[i]) if err != nil { log.Fatalf("Failed to compute key info for %s: %v", config.Logs[i].ShortName, err) } } tmpl, err := template.New("html").Parse(htmlTemplate) if err != nil { log.Fatalf("Failed to parse template: %v", err) } // Write HTML file to each log's local directory for _, logEntry := range config.Logs { indexPath := fmt.Sprintf("%s/index.html", logEntry.LocalDirectory) file, err := os.Create(indexPath) if err != nil { log.Fatalf("Failed to create %s: %v", indexPath, err) } err = tmpl.Execute(file, config) if err != nil { file.Close() log.Fatalf("Failed to write HTML to %s: %v", indexPath, err) } file.Close() fmt.Printf("Generated %s\n", indexPath) // Generate log.v3.json for this log jsonPath := filepath.Join(logEntry.LocalDirectory, "log.v3.json") err = generateLogJSON(logEntry, jsonPath) if err != nil { log.Fatalf("Failed to generate %s: %v", jsonPath, err) } fmt.Printf("Generated %s\n", jsonPath) } } func computeKeyInfo(logEntry *Log) error { // Read the private key file keyData, err := os.ReadFile(logEntry.Secret) if err != nil { return fmt.Errorf("failed to read key file: %v", err) } // Parse PEM block block, _ := pem.Decode(keyData) if block == nil { return fmt.Errorf("failed to decode PEM block") } // Parse EC private key privKey, err := x509.ParseECPrivateKey(block.Bytes) if err != nil { return fmt.Errorf("failed to parse EC private key: %v", err) } // Extract public key pubKey := &privKey.PublicKey // Convert public key to DER format pubKeyDER, err := x509.MarshalPKIXPublicKey(pubKey) if err != nil { return fmt.Errorf("failed to marshal public key: %v", err) } // Create PEM format pubKeyPEM := pem.EncodeToMemory(&pem.Block{ Type: "PUBLIC KEY", Bytes: pubKeyDER, }) // Compute Log ID (SHA-256 of the DER-encoded public key) logIDBytes := sha256.Sum256(pubKeyDER) logID := base64.StdEncoding.EncodeToString(logIDBytes[:]) // Base64 encode DER for download link pubKeyDERB64 := base64.StdEncoding.EncodeToString(pubKeyDER) // Set computed fields logEntry.LogID = logID logEntry.PublicKeyPEM = string(pubKeyPEM) logEntry.PublicKeyDERB64 = pubKeyDERB64 logEntry.PublicKeyBase64 = pubKeyDERB64 // Same as DER base64 for JSON return nil } type LogV3JSON struct { Description string `json:"description"` SubmissionURL string `json:"submission_url"` MonitoringURL string `json:"monitoring_url"` TemporalInterval TemporalInterval `json:"temporal_interval"` LogID string `json:"log_id"` Key string `json:"key"` MMD int `json:"mmd"` } type TemporalInterval struct { StartInclusive string `json:"start_inclusive"` EndExclusive string `json:"end_exclusive"` } func generateLogJSON(logEntry Log, outputPath string) error { logJSON := LogV3JSON{ Description: fmt.Sprintf("%s.log.ct.ipng.ch", logEntry.ShortName), SubmissionURL: fmt.Sprintf("%s/", logEntry.SubmissionPrefix), MonitoringURL: fmt.Sprintf("%s/", logEntry.MonitoringPrefix), TemporalInterval: TemporalInterval{ StartInclusive: logEntry.NotAfterStart.Format("2006-01-02T15:04:05Z"), EndExclusive: logEntry.NotAfterLimit.Format("2006-01-02T15:04:05Z"), }, LogID: logEntry.LogID, Key: logEntry.PublicKeyBase64, MMD: 60, // Default MMD of 60 seconds } jsonData, err := json.MarshalIndent(logJSON, "", " ") if err != nil { return fmt.Errorf("failed to marshal JSON: %v", err) } err = os.WriteFile(outputPath, jsonData, 0644) if err != nil { return fmt.Errorf("failed to write JSON file: %v", err) } return nil } func generateEnv(yamlFile string) { config := loadConfig(yamlFile) // Check that all local directories exist for _, logEntry := range config.Logs { if _, err := os.Stat(logEntry.LocalDirectory); os.IsNotExist(err) { log.Fatalf("User is required to create %s", logEntry.LocalDirectory) } } // Generate .env file for each log for _, logEntry := range config.Logs { envPath := filepath.Join(logEntry.LocalDirectory, ".env") // Create combined roots.pem file rootsPemPath := filepath.Join(logEntry.LocalDirectory, "roots.pem") err := createCombinedRootsPem(config.Roots, logEntry.ExtraRoots, rootsPemPath) if err != nil { log.Fatalf("Failed to create %s: %v", rootsPemPath, err) } fmt.Printf("Generated %s\n", rootsPemPath) // Build TESSERACT_ARGS string args := []string{ fmt.Sprintf("--private_key=%s", logEntry.Secret), fmt.Sprintf("--origin=%s.log.ct.ipng.ch", logEntry.ShortName), fmt.Sprintf("--storage_dir=%s", logEntry.LocalDirectory), fmt.Sprintf("--roots_pem_file=%s", rootsPemPath), } tesseractArgs := strings.Join(args, " ") envContent := fmt.Sprintf("TESSERACT_ARGS=\"%s\"\n", tesseractArgs) err = os.WriteFile(envPath, []byte(envContent), 0644) if err != nil { log.Fatalf("Failed to write %s: %v", envPath, err) } fmt.Printf("Generated %s\n", envPath) } } func createCombinedRootsPem(rootsFile, extraRootsFile, outputPath string) error { // Create output file outputFile, err := os.Create(outputPath) if err != nil { return fmt.Errorf("failed to create output file: %v", err) } defer outputFile.Close() // Copy main roots file if rootsFile != "" { rootsData, err := os.Open(rootsFile) if err != nil { return fmt.Errorf("failed to open roots file %s: %v", rootsFile, err) } defer rootsData.Close() _, err = io.Copy(outputFile, rootsData) if err != nil { return fmt.Errorf("failed to copy roots file: %v", err) } } // Append extra roots file if it exists if extraRootsFile != "" { extraRootsData, err := os.Open(extraRootsFile) if err != nil { return fmt.Errorf("failed to open extra roots file %s: %v", extraRootsFile, err) } defer extraRootsData.Close() _, err = io.Copy(outputFile, extraRootsData) if err != nil { return fmt.Errorf("failed to copy extra roots file: %v", err) } } return nil } type CTLogRootsResponse struct { Certificates []string `json:"certificates"` } func generateRoots(args []string) { sourceURL := "https://rennet2027h2.log.ct.ipng.ch/" outputFile := "roots.pem" // Parse command line arguments for i := 0; i < len(args); i++ { switch args[i] { case "--source": if i+1 >= len(args) { log.Fatal("--source flag requires a URL argument") } sourceURL = args[i+1] i++ // Skip the next argument since we used it case "--output": if i+1 >= len(args) { log.Fatal("--output flag requires a filename argument") } outputFile = args[i+1] i++ // Skip the next argument since we used it default: log.Fatalf("Unknown argument: %s", args[i]) } } // Ensure source URL ends with / if !strings.HasSuffix(sourceURL, "/") { sourceURL += "/" } // Construct the get-roots URL getRootsURL := sourceURL + "ct/v1/get-roots" // Fetch roots from CT log fmt.Printf("Fetching roots from: %s\n", getRootsURL) resp, err := http.Get(getRootsURL) if err != nil { log.Fatalf("Failed to fetch roots: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Fatalf("HTTP request failed with status: %d", resp.StatusCode) } // Parse JSON response var rootsResp CTLogRootsResponse err = json.NewDecoder(resp.Body).Decode(&rootsResp) if err != nil { log.Fatalf("Failed to parse JSON response: %v", err) } // Create output file outFile, err := os.Create(outputFile) if err != nil { log.Fatalf("Failed to create output file %s: %v", outputFile, err) } defer outFile.Close() // Write each certificate as PEM for _, certBase64 := range rootsResp.Certificates { // Decode base64 certificate certBytes, err := base64.StdEncoding.DecodeString(certBase64) if err != nil { log.Fatalf("Failed to decode certificate: %v", err) } // Create PEM block pemBlock := &pem.Block{ Type: "CERTIFICATE", Bytes: certBytes, } // Write PEM to file err = pem.Encode(outFile, pemBlock) if err != nil { log.Fatalf("Failed to write PEM certificate: %v", err) } } fmt.Printf("Successfully wrote %d certificates to %s\n", len(rootsResp.Certificates), outputFile) } func generateKeys(yamlFile string) { config := loadConfig(yamlFile) // Generate keys for each log for _, logEntry := range config.Logs { // Check if key already exists if _, err := os.Stat(logEntry.Secret); err == nil { fmt.Printf("Key already exists: %s (skipped)\n", logEntry.Secret) continue } // Generate new prime256v1 key privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { log.Fatalf("Failed to generate key for %s: %v", logEntry.ShortName, err) } // Marshal private key to DER format privKeyDER, err := x509.MarshalECPrivateKey(privKey) if err != nil { log.Fatalf("Failed to marshal private key for %s: %v", logEntry.ShortName, err) } // Create PEM block privKeyPEM := pem.EncodeToMemory(&pem.Block{ Type: "EC PRIVATE KEY", Bytes: privKeyDER, }) // Ensure directory exists if err := os.MkdirAll(filepath.Dir(logEntry.Secret), 0755); err != nil { log.Fatalf("Failed to create directory for %s: %v", logEntry.Secret, err) } // Write key to file err = os.WriteFile(logEntry.Secret, privKeyPEM, 0600) if err != nil { log.Fatalf("Failed to write key file %s: %v", logEntry.Secret, err) } fmt.Printf("Generated %s\n", logEntry.Secret) } }