Refactor each command in its own file
This commit is contained in:
91
tesseract/genconf/env.go
Normal file
91
tesseract/genconf/env.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
235
tesseract/genconf/html.go
Normal file
235
tesseract/genconf/html.go
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
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>
|
||||||
|
`
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
57
tesseract/genconf/key.go
Normal file
57
tesseract/genconf/key.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@@ -1,23 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/pem"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -51,84 +38,6 @@ type Log struct {
|
|||||||
PublicKeyBase64 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() {
|
func main() {
|
||||||
configFile := flag.String("c", "./tesseract-staging.yaml", "Path to the YAML configuration file")
|
configFile := flag.String("c", "./tesseract-staging.yaml", "Path to the YAML configuration file")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@@ -197,371 +106,3 @@ func showHelp() {
|
|||||||
fmt.Printf(" Options: --source <url> (default: https://rennet2027h2.log.ct.ipng.ch/)\n")
|
fmt.Printf(" Options: --source <url> (default: https://rennet2027h2.log.ct.ipng.ch/)\n")
|
||||||
fmt.Printf(" --output <file> (default: roots.pem)\n\n")
|
fmt.Printf(" --output <file> (default: roots.pem)\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
|
|
||||||
}
|
|
||||||
|
|
||||||
type CTLogRootsResponse struct {
|
|
||||||
Certificates []string `json:"certificates"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateRoots(args []string) {
|
|
||||||
sourceURL := "https://rennet2027h2.log.ct.ipng.ch/"
|
|
||||||
outputFile := "roots.pem"
|
|
||||||
|
|
||||||
// Parse command line arguments
|
|
||||||
for i := 0; i < len(args); i++ {
|
|
||||||
switch args[i] {
|
|
||||||
case "--source":
|
|
||||||
if i+1 >= len(args) {
|
|
||||||
log.Fatal("--source flag requires a URL argument")
|
|
||||||
}
|
|
||||||
sourceURL = args[i+1]
|
|
||||||
i++ // Skip the next argument since we used it
|
|
||||||
case "--output":
|
|
||||||
if i+1 >= len(args) {
|
|
||||||
log.Fatal("--output flag requires a filename argument")
|
|
||||||
}
|
|
||||||
outputFile = args[i+1]
|
|
||||||
i++ // Skip the next argument since we used it
|
|
||||||
default:
|
|
||||||
log.Fatalf("Unknown argument: %s", args[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure source URL ends with /
|
|
||||||
if !strings.HasSuffix(sourceURL, "/") {
|
|
||||||
sourceURL += "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the get-roots URL
|
|
||||||
getRootsURL := sourceURL + "ct/v1/get-roots"
|
|
||||||
|
|
||||||
// Fetch roots from CT log
|
|
||||||
fmt.Printf("Fetching roots from: %s\n", getRootsURL)
|
|
||||||
resp, err := http.Get(getRootsURL)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to fetch roots: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
log.Fatalf("HTTP request failed with status: %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse JSON response
|
|
||||||
var rootsResp CTLogRootsResponse
|
|
||||||
err = json.NewDecoder(resp.Body).Decode(&rootsResp)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to parse JSON response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create output file
|
|
||||||
outFile, err := os.Create(outputFile)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to create output file %s: %v", outputFile, err)
|
|
||||||
}
|
|
||||||
defer outFile.Close()
|
|
||||||
|
|
||||||
// Write each certificate as PEM
|
|
||||||
for _, certBase64 := range rootsResp.Certificates {
|
|
||||||
// Decode base64 certificate
|
|
||||||
certBytes, err := base64.StdEncoding.DecodeString(certBase64)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to decode certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create PEM block
|
|
||||||
pemBlock := &pem.Block{
|
|
||||||
Type: "CERTIFICATE",
|
|
||||||
Bytes: certBytes,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write PEM to file
|
|
||||||
err = pem.Encode(outFile, pemBlock)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to write PEM certificate: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Successfully wrote %d certificates to %s\n", len(rootsResp.Certificates), outputFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
98
tesseract/genconf/roots.go
Normal file
98
tesseract/genconf/roots.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CTLogRootsResponse struct {
|
||||||
|
Certificates []string `json:"certificates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRoots(args []string) {
|
||||||
|
sourceURL := "https://rennet2027h2.log.ct.ipng.ch/"
|
||||||
|
outputFile := "roots.pem"
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
switch args[i] {
|
||||||
|
case "--source":
|
||||||
|
if i+1 >= len(args) {
|
||||||
|
log.Fatal("--source flag requires a URL argument")
|
||||||
|
}
|
||||||
|
sourceURL = args[i+1]
|
||||||
|
i++ // Skip the next argument since we used it
|
||||||
|
case "--output":
|
||||||
|
if i+1 >= len(args) {
|
||||||
|
log.Fatal("--output flag requires a filename argument")
|
||||||
|
}
|
||||||
|
outputFile = args[i+1]
|
||||||
|
i++ // Skip the next argument since we used it
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown argument: %s", args[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure source URL ends with /
|
||||||
|
if !strings.HasSuffix(sourceURL, "/") {
|
||||||
|
sourceURL += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the get-roots URL
|
||||||
|
getRootsURL := sourceURL + "ct/v1/get-roots"
|
||||||
|
|
||||||
|
// Fetch roots from CT log
|
||||||
|
fmt.Printf("Fetching roots from: %s\n", getRootsURL)
|
||||||
|
resp, err := http.Get(getRootsURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to fetch roots: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Fatalf("HTTP request failed with status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON response
|
||||||
|
var rootsResp CTLogRootsResponse
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&rootsResp)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to parse JSON response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create output file
|
||||||
|
outFile, err := os.Create(outputFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create output file %s: %v", outputFile, err)
|
||||||
|
}
|
||||||
|
defer outFile.Close()
|
||||||
|
|
||||||
|
// Write each certificate as PEM
|
||||||
|
for _, certBase64 := range rootsResp.Certificates {
|
||||||
|
// Decode base64 certificate
|
||||||
|
certBytes, err := base64.StdEncoding.DecodeString(certBase64)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to decode certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create PEM block
|
||||||
|
pemBlock := &pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: certBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write PEM to file
|
||||||
|
err = pem.Encode(outFile, pemBlock)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to write PEM certificate: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Successfully wrote %d certificates to %s\n", len(rootsResp.Certificates), outputFile)
|
||||||
|
}
|
Reference in New Issue
Block a user