diff --git a/.gitignore b/.gitignore index 1a5321a..89a1df9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ tesseract-genconf +roots.pem diff --git a/tesseract/genconf/main.go b/tesseract/genconf/main.go index 8ec68ce..f2c76ea 100644 --- a/tesseract/genconf/main.go +++ b/tesseract/genconf/main.go @@ -13,6 +13,7 @@ import ( "fmt" "io" "log" + "net/http" "os" "path/filepath" "strings" @@ -145,6 +146,8 @@ func main() { 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() @@ -163,7 +166,7 @@ func loadConfig(yamlFile string) 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 { @@ -173,7 +176,7 @@ func loadConfig(yamlFile string) Config { config.Logs[i].Period = 200 } } - + return config } @@ -190,6 +193,9 @@ func showHelp() { 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) { @@ -430,6 +436,92 @@ func createCombinedRootsPem(rootsFile, extraRootsFile, outputPath string) error 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)