First stab at tesseract config generator. Generate HTML files in each of the log dirs
This commit is contained in:
259
tesseract/genconf/main.go
Normal file
259
tesseract/genconf/main.go
Normal file
@@ -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 = `<!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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
Reference in New Issue
Block a user