Files
cheese/tesseract/genconf/main.go
2025-08-24 10:51:15 +02:00

314 lines
8.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"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
PublicKeyBase64 string
}
const htmlTemplate = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TesseraCT</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bitter&family=Raleway&family=Source+Code+Pro&display=swap" rel="stylesheet">
<style>
.container {
width: auto;
max-width: 700px;
padding: 0 15px;
margin: 80px auto;
}
body {
font-family: "Raleway", sans-serif;
line-height: 1.4;
}
h1, h2, h3 {
font-family: "Bitter", serif;
}
code {
font-family: "Source Code Pro", monospace;
-webkit-font-smoothing: antialiased;
}
.response {
white-space: wrap;
word-break: break-all;
}
</style>
</head>
<body>
<div class="container">
<p align="center">
<img alt="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." width="250" height="278" src="https://ipng.ch/assets/ctlog/tesseract-logo.png"> </p>
<p>
This is a <a href="https://github.com/transparency-dev/tesseract">TesseraCT</a> Certificate Transparency log instance.
<hr>
<p>
The following logs are active.
{{range .Logs}}
<h2>{{.ShortName}}.log.ct.ipng.ch</h2>
<p>
Log ID: <code>{{.LogID}}</code><br>
Monitoring prefix: <code>{{.MonitoringPrefix}}/</code><br>
Submission prefix: <code>{{.SubmissionPrefix}}/</code><br>
Interval: {{.NotAfterStart.Format "2006-01-02T15:04:05Z"}} {{.NotAfterLimit.Format "2006-01-02T15:04:05Z"}}<br>
Links: <a href="{{.MonitoringPrefix}}/checkpoint">checkpoint</a>
<a href="data:application/octet-stream;base64,{{.PublicKeyDERB64}}"
download="{{.ShortName}}.der">key</a>
<a href="{{.SubmissionPrefix}}/ct/v1/get-roots">get-roots</a>
<a href="{{.SubmissionPrefix}}/log.v3.json">json</a><br>
Ratelimit: {{.PoolSize}} req/s
<pre><code>{{.PublicKeyPEM}}</code></pre>
{{end}}
</body>
</html>
`
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)
// 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
}