diff --git a/tesseract/genconf/env.go b/tesseract/genconf/env.go new file mode 100644 index 0000000..fcef8b8 --- /dev/null +++ b/tesseract/genconf/env.go @@ -0,0 +1,91 @@ +package main + +import ( + "fmt" + "io" + "log" + "os" + "path/filepath" + "strings" +) + +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 +} diff --git a/tesseract/genconf/html.go b/tesseract/genconf/html.go new file mode 100644 index 0000000..20ed122 --- /dev/null +++ b/tesseract/genconf/html.go @@ -0,0 +1,235 @@ +package main + +import ( + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "log" + "os" + "path/filepath" + "text/template" +) + +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}} + + + +` + +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 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 +} + +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 +} diff --git a/tesseract/genconf/key.go b/tesseract/genconf/key.go new file mode 100644 index 0000000..f49d1dc --- /dev/null +++ b/tesseract/genconf/key.go @@ -0,0 +1,57 @@ +package main + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "fmt" + "log" + "os" + "path/filepath" +) + +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) + } +} diff --git a/tesseract/genconf/main.go b/tesseract/genconf/main.go index f2c76ea..aaba768 100644 --- a/tesseract/genconf/main.go +++ b/tesseract/genconf/main.go @@ -1,23 +1,10 @@ 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" @@ -51,84 +38,6 @@ type Log struct { 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() @@ -197,371 +106,3 @@ func showHelp() { 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) - } -} diff --git a/tesseract/genconf/roots.go b/tesseract/genconf/roots.go new file mode 100644 index 0000000..1ed7ff5 --- /dev/null +++ b/tesseract/genconf/roots.go @@ -0,0 +1,98 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "log" + "net/http" + "os" + "strings" +) + +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) +}