Files
cheese/tesseract/genconf/main.go
Pim van Pelt 9b1dd06acf go fmt
2025-08-24 11:11:25 +02:00

465 lines
13 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/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"flag"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"text/template"
"time"
"gopkg.in/yaml.v3"
)
type Config struct {
Listen []string `yaml:"listen"`
Checkpoints string `yaml:"checkpoints"`
Roots string `yaml:"roots"`
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="{{.MonitoringPrefix}}/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 {
showHelp()
return
}
switch args[0] {
case "gen-html":
generateHTML(*configFile)
case "gen-env":
generateEnv(*configFile)
case "gen-key":
generateKeys(*configFile)
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", args[0])
showHelp()
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 showHelp() {
fmt.Printf("Usage: %s [options] <command>\n\n", os.Args[0])
fmt.Printf("Options:\n")
fmt.Printf(" -c <file> Path to YAML configuration file (default: ./tesseract-staging.yaml)\n\n")
fmt.Printf("Commands:\n")
fmt.Printf(" gen-html Generate index.html and log.v3.json files in each log's localdirectory.\n")
fmt.Printf(" Creates HTML pages with log information and CT log metadata JSON.\n")
fmt.Printf(" Computes LOG_ID and public keys from private keys.\n\n")
fmt.Printf(" gen-env Generate .env files and combined roots.pem in each log's localdirectory.\n")
fmt.Printf(" Creates TESSERACT_ARGS environment variable with command line flags.\n")
fmt.Printf(" Combines global roots and log-specific extraroots into roots.pem.\n\n")
fmt.Printf(" gen-key Generate prime256v1 private keys for each log (only if they don't exist).\n")
fmt.Printf(" Creates EC private key files at the path specified in log.secret.\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
}
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)
}
}