diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e4166cc --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module cheese + +go 1.24.4 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tesseract/genconf/main.go b/tesseract/genconf/main.go new file mode 100644 index 0000000..28ed4a4 --- /dev/null +++ b/tesseract/genconf/main.go @@ -0,0 +1,259 @@ +package main + +import ( + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "flag" + "fmt" + "log" + "os" + "text/template" + "time" + + "gopkg.in/yaml.v3" +) + +type Config struct { + Listen []string `yaml:"listen"` + Checkpoints string `yaml:"checkpoints"` + 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 +} + +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 { + showConfig(*configFile) + return + } + + switch args[0] { + case "gen-html": + generateHTML(*configFile) + default: + fmt.Fprintf(os.Stderr, "Unknown command: %s\n", args[0]) + 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) + } + return config +} + +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) + } +} + +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 + + return nil +}