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 = ` + + +
+ + + +
+
+ This is a TesseraCT Certificate Transparency log instance. + +
+ The following logs are active. + + {{range .Logs}} + +
+ 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
+}