233 lines
6.7 KiB
Go
233 lines
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"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="260" height="250" 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.
|
|
|
|
<p>
|
|
<a href="/metrics">Metrics</a> are available.
|
|
|
|
<hr>
|
|
|
|
<p>
|
|
The following logs are active:
|
|
|
|
{{range .Logs}}
|
|
|
|
<h2>{{.Origin}}</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, wantDiff bool, allowWrite bool, useColor bool) {
|
|
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)
|
|
|
|
// Execute template to buffer
|
|
var buf bytes.Buffer
|
|
err := tmpl.Execute(&buf, config)
|
|
if err != nil {
|
|
log.Fatalf("Failed to execute HTML template for %s: %v", indexPath, err)
|
|
}
|
|
|
|
// Write file with status
|
|
err = writeFileWithStatus(indexPath, buf.Bytes(), wantDiff, allowWrite, useColor)
|
|
if err != nil {
|
|
log.Fatalf("Failed to write HTML to %s: %v", indexPath, err)
|
|
}
|
|
|
|
// Generate log.v3.json for this log
|
|
jsonPath := filepath.Join(logEntry.LocalDirectory, "log.v3.json")
|
|
err = generateLogJSONWithStatus(logEntry, jsonPath, wantDiff, allowWrite, useColor)
|
|
if err != nil {
|
|
log.Fatalf("Failed to generate %s: %v", jsonPath, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 generateLogJSONWithStatus(logEntry Log, outputPath string, wantDiff bool, allowWrite bool, useColor bool) error {
|
|
logJSON := LogV3JSON{
|
|
Description: logEntry.Origin,
|
|
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)
|
|
}
|
|
|
|
return writeFileWithStatus(outputPath, jsonData, wantDiff, allowWrite, useColor)
|
|
}
|