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 = `
+
+
+
+
+
+
+
+
+ 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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 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)
+}