Compare commits
5 Commits
custom-dom
...
105a245239
Author | SHA1 | Date | |
---|---|---|---|
|
105a245239 | ||
|
7b3639ad69 | ||
|
0503370489 | ||
|
6b11481739 | ||
|
d027ec9108 |
30
README.md
30
README.md
@@ -36,23 +36,23 @@ logs:
|
||||
3. **Generate private keys:**
|
||||
```bash
|
||||
mkdir -p /etc/tesseract/keys
|
||||
./tesseract-genconf -c config.yaml gen-key
|
||||
./tesseract-genconf -c config.yaml --write gen-key
|
||||
```
|
||||
|
||||
4. **Create directories and generate environment files:**
|
||||
```bash
|
||||
mkdir -p /var/lib/tesseract/example2025h1/data
|
||||
./tesseract-genconf -c config.yaml gen-env
|
||||
./tesseract-genconf -c config.yaml --write gen-env
|
||||
```
|
||||
|
||||
5. **Generate HTML and JSON files:**
|
||||
```bash
|
||||
./tesseract-genconf -c config.yaml gen-html
|
||||
./tesseract-genconf -c config.yaml --write gen-html
|
||||
```
|
||||
|
||||
6. **Generate nginx configuration files:**
|
||||
```bash
|
||||
./tesseract-genconf -c config.yaml gen-nginx
|
||||
./tesseract-genconf -c config.yaml --write gen-nginx
|
||||
```
|
||||
|
||||
The port from the main `listen:` field will be used in the NGINX server blocks (in our case
|
||||
@@ -66,3 +66,25 @@ The port from the main `listen:` field will be used in the NGINX server blocks (
|
||||
# For production environment, take the ccadb 'production' roots
|
||||
./tesseract-genconf gen-roots --source https://gouda2027h2.log.ct.ipng.ch/ --output roots-production.pem
|
||||
```
|
||||
|
||||
### Safe File Operations with `--diff` and `--write`
|
||||
|
||||
The `tesseract-genconf` tool includes safety features to prevent accidental file modifications:
|
||||
|
||||
- **`--diff`**: Shows colored unified diffs of what would change without writing files
|
||||
- **`--write`**: Required flag to actually write files to disk
|
||||
- **`--no-color`**: Disables colored diff output (useful for redirecting to files)
|
||||
|
||||
**Recommended workflow:**
|
||||
```bash
|
||||
# 1. First, preview changes with --diff
|
||||
./tesseract-genconf -c config.yaml --diff gen-html
|
||||
|
||||
# 2. Review the colored diff output, then apply changes
|
||||
./tesseract-genconf -c config.yaml --write gen-html
|
||||
|
||||
# 3. Or combine both to see diffs and write files
|
||||
./tesseract-genconf -c config.yaml --diff --write gen-html
|
||||
```
|
||||
|
||||
**Note:** Flags must come before the command name (e.g., `--diff gen-html`, not `gen-html --diff`).
|
||||
|
5
go.mod
5
go.mod
@@ -3,3 +3,8 @@ module cheese
|
||||
go 1.24.4
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
||||
|
||||
require (
|
||||
github.com/hexops/gotextdiff v1.0.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
)
|
||||
|
4
go.sum
4
go.sum
@@ -1,3 +1,7 @@
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func generateEnv(yamlFile string) {
|
||||
func generateEnv(yamlFile string, wantDiff bool, allowWrite bool, useColor bool) {
|
||||
config := loadConfig(yamlFile)
|
||||
|
||||
// Check that all local directories exist
|
||||
@@ -24,7 +24,7 @@ func generateEnv(yamlFile string) {
|
||||
|
||||
// Create combined roots.pem file
|
||||
rootsPemPath := filepath.Join(logEntry.LocalDirectory, "roots.pem")
|
||||
err := createCombinedRootsPemWithStatus(config.Roots, logEntry.ExtraRoots, rootsPemPath)
|
||||
err := createCombinedRootsPemWithStatus(config.Roots, logEntry.ExtraRoots, rootsPemPath, wantDiff, allowWrite, useColor)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create %s: %v", rootsPemPath, err)
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func generateEnv(yamlFile string) {
|
||||
// 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("--origin=%s", logEntry.Origin),
|
||||
fmt.Sprintf("--storage_dir=%s", logEntry.LocalDirectory),
|
||||
fmt.Sprintf("--roots_pem_file=%s", rootsPemPath),
|
||||
}
|
||||
@@ -53,14 +53,14 @@ func generateEnv(yamlFile string) {
|
||||
tesseractArgs := strings.Join(args, " ")
|
||||
envContent := fmt.Sprintf("TESSERACT_ARGS=\"%s\"\nOTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318\n", tesseractArgs)
|
||||
|
||||
err = writeFileWithStatus(envPath, []byte(envContent))
|
||||
err = writeFileWithStatus(envPath, []byte(envContent), wantDiff, allowWrite, useColor)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to write %s: %v", envPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createCombinedRootsPemWithStatus(rootsFile, extraRootsFile, outputPath string) error {
|
||||
func createCombinedRootsPemWithStatus(rootsFile, extraRootsFile, outputPath string, wantDiff bool, allowWrite bool, useColor bool) error {
|
||||
// Read main roots file
|
||||
var combinedContent []byte
|
||||
if rootsFile != "" {
|
||||
@@ -80,5 +80,5 @@ func createCombinedRootsPemWithStatus(rootsFile, extraRootsFile, outputPath stri
|
||||
combinedContent = append(combinedContent, extraRootsData...)
|
||||
}
|
||||
|
||||
return writeFileWithStatus(outputPath, combinedContent)
|
||||
return writeFileWithStatus(outputPath, combinedContent, wantDiff, allowWrite, useColor)
|
||||
}
|
||||
|
219
tesseract/genconf/env_test.go
Normal file
219
tesseract/genconf/env_test.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateEnv(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create test directories
|
||||
testLogDir := filepath.Join(tmpDir, "test-log")
|
||||
testLog2Dir := filepath.Join(tmpDir, "test-log-2")
|
||||
err := os.MkdirAll(testLogDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = os.MkdirAll(testLog2Dir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create test roots files
|
||||
rootsFile := filepath.Join(tmpDir, "roots.pem")
|
||||
extraRootsFile := filepath.Join(tmpDir, "extra-roots.pem")
|
||||
|
||||
rootsContent := `-----BEGIN CERTIFICATE-----
|
||||
MIIBkTCB+wIJAKZJ...
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
extraRootsContent := `-----BEGIN CERTIFICATE-----
|
||||
MIIBkTCB+wIJAKZK...
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
err = os.WriteFile(rootsFile, []byte(rootsContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = os.WriteFile(extraRootsFile, []byte(extraRootsContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create test config
|
||||
configContent := `listen:
|
||||
- ":8080"
|
||||
roots: "` + rootsFile + `"
|
||||
logs:
|
||||
- shortname: "test-log"
|
||||
submissionprefix: "https://example.com/submit"
|
||||
monitoringprefix: "https://example.com/monitor"
|
||||
extraroots: "` + extraRootsFile + `"
|
||||
secret: "test-data/test-log.key"
|
||||
localdirectory: "` + testLogDir + `"
|
||||
listen: ":8081"
|
||||
notafterstart: "2024-01-01T00:00:00Z"
|
||||
notafterlimit: "2025-01-01T00:00:00Z"
|
||||
- shortname: "test-log-2"
|
||||
submissionprefix: "https://log2.example.com/submit"
|
||||
monitoringprefix: "https://log2.example.com/monitor"
|
||||
secret: "test-data/test-log-2.key"
|
||||
localdirectory: "` + testLog2Dir + `"`
|
||||
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
err = os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Run generateEnv
|
||||
generateEnv(configFile, false, true, false)
|
||||
|
||||
// Verify .env file was created for first log
|
||||
envFile := filepath.Join(testLogDir, ".env")
|
||||
envContent, err := os.ReadFile(envFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read .env file: %v", err)
|
||||
}
|
||||
|
||||
envStr := string(envContent)
|
||||
|
||||
// Check TESSERACT_ARGS contains expected values
|
||||
if !strings.Contains(envStr, "TESSERACT_ARGS=") {
|
||||
t.Error("Expected TESSERACT_ARGS in .env file")
|
||||
}
|
||||
if !strings.Contains(envStr, "--private_key=test-data/test-log.key") {
|
||||
t.Error("Expected private_key argument in TESSERACT_ARGS")
|
||||
}
|
||||
if !strings.Contains(envStr, "--origin=example.com") {
|
||||
t.Error("Expected origin argument in TESSERACT_ARGS")
|
||||
}
|
||||
if !strings.Contains(envStr, "--storage_dir="+testLogDir) {
|
||||
t.Error("Expected storage_dir argument in TESSERACT_ARGS")
|
||||
}
|
||||
if !strings.Contains(envStr, "--http_endpoint=:8081") {
|
||||
t.Error("Expected http_endpoint argument in TESSERACT_ARGS")
|
||||
}
|
||||
if !strings.Contains(envStr, "--not_after_start=2024-01-01T00:00:00Z") {
|
||||
t.Error("Expected not_after_start argument in TESSERACT_ARGS")
|
||||
}
|
||||
if !strings.Contains(envStr, "--not_after_limit=2025-01-01T00:00:00Z") {
|
||||
t.Error("Expected not_after_limit argument in TESSERACT_ARGS")
|
||||
}
|
||||
if !strings.Contains(envStr, "OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318") {
|
||||
t.Error("Expected OTEL_EXPORTER_OTLP_ENDPOINT in .env file")
|
||||
}
|
||||
|
||||
// Verify combined roots.pem was created
|
||||
combinedRootsFile := filepath.Join(testLogDir, "roots.pem")
|
||||
combinedContent, err := os.ReadFile(combinedRootsFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read combined roots.pem: %v", err)
|
||||
}
|
||||
|
||||
combinedStr := string(combinedContent)
|
||||
if !strings.Contains(combinedStr, "MIIBkTCB+wIJAKZJ") {
|
||||
t.Error("Expected content from main roots file")
|
||||
}
|
||||
if !strings.Contains(combinedStr, "MIIBkTCB+wIJAKZK") {
|
||||
t.Error("Expected content from extra roots file")
|
||||
}
|
||||
|
||||
// Verify second log's .env file was created
|
||||
env2File := filepath.Join(testLog2Dir, ".env")
|
||||
env2Content, err := os.ReadFile(env2File)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read second .env file: %v", err)
|
||||
}
|
||||
|
||||
env2Str := string(env2Content)
|
||||
if !strings.Contains(env2Str, "--origin=log2.example.com") {
|
||||
t.Error("Expected correct origin for second log")
|
||||
}
|
||||
if strings.Contains(env2Str, "--http_endpoint=") {
|
||||
t.Error("Second log should not have http_endpoint (not specified)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCombinedRootsPemWithStatus(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Test case 1: Both files exist
|
||||
rootsFile := filepath.Join(tmpDir, "roots.pem")
|
||||
extraFile := filepath.Join(tmpDir, "extra.pem")
|
||||
outputFile := filepath.Join(tmpDir, "combined.pem")
|
||||
|
||||
rootsContent := "ROOT CERT CONTENT\n"
|
||||
extraContent := "EXTRA CERT CONTENT\n"
|
||||
|
||||
err := os.WriteFile(rootsFile, []byte(rootsContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = os.WriteFile(extraFile, []byte(extraContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = createCombinedRootsPemWithStatus(rootsFile, extraFile, outputFile, false, true, false)
|
||||
if err != nil {
|
||||
t.Errorf("createCombinedRootsPemWithStatus() error = %v", err)
|
||||
}
|
||||
|
||||
combinedContent, err := os.ReadFile(outputFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := rootsContent + extraContent
|
||||
if string(combinedContent) != expected {
|
||||
t.Errorf("Combined content = %s, want %s", string(combinedContent), expected)
|
||||
}
|
||||
|
||||
// Test case 2: Only roots file exists
|
||||
outputFile2 := filepath.Join(tmpDir, "combined2.pem")
|
||||
err = createCombinedRootsPemWithStatus(rootsFile, "", outputFile2, false, true, false)
|
||||
if err != nil {
|
||||
t.Errorf("createCombinedRootsPemWithStatus() with empty extra file error = %v", err)
|
||||
}
|
||||
|
||||
combinedContent2, err := os.ReadFile(outputFile2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(combinedContent2) != rootsContent {
|
||||
t.Errorf("Combined content = %s, want %s", string(combinedContent2), rootsContent)
|
||||
}
|
||||
|
||||
// Test case 3: Extra file doesn't exist (should error)
|
||||
outputFile3 := filepath.Join(tmpDir, "combined3.pem")
|
||||
err = createCombinedRootsPemWithStatus(rootsFile, "nonexistent.pem", outputFile3, false, true, false)
|
||||
if err == nil {
|
||||
t.Error("Expected error when extra file doesn't exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateEnvMissingDirectory(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create config with non-existent directory
|
||||
configContent := `logs:
|
||||
- shortname: "test-log"
|
||||
submissionprefix: "https://example.com/submit"
|
||||
secret: "test.key"
|
||||
localdirectory: "/nonexistent/directory"`
|
||||
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
err := os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Should call log.Fatalf which exits the program
|
||||
// We can't easily test this without subprocess, so we'll skip it
|
||||
t.Skip("Cannot easily test log.Fatalf without subprocess")
|
||||
}
|
@@ -74,7 +74,7 @@ const htmlTemplate = `<!DOCTYPE html>
|
||||
|
||||
{{range .Logs}}
|
||||
|
||||
<h2>{{.ShortName}}.log.ct.ipng.ch</h2>
|
||||
<h2>{{.Origin}}</h2>
|
||||
|
||||
<p>
|
||||
Log ID: <code>{{.LogID}}</code><br>
|
||||
@@ -95,6 +95,7 @@ const htmlTemplate = `<!DOCTYPE html>
|
||||
</html>
|
||||
`
|
||||
|
||||
// LogV3JSON represents the Certificate Transparency log v3 JSON metadata format
|
||||
type LogV3JSON struct {
|
||||
Description string `json:"description"`
|
||||
SubmissionURL string `json:"submission_url"`
|
||||
@@ -105,12 +106,13 @@ type LogV3JSON struct {
|
||||
MMD int `json:"mmd"`
|
||||
}
|
||||
|
||||
// TemporalInterval represents the time range for a Certificate Transparency log
|
||||
type TemporalInterval struct {
|
||||
StartInclusive string `json:"start_inclusive"`
|
||||
EndExclusive string `json:"end_exclusive"`
|
||||
}
|
||||
|
||||
func generateHTML(yamlFile string) {
|
||||
func generateHTML(yamlFile string, wantDiff bool, allowWrite bool, useColor bool) {
|
||||
config := loadConfig(yamlFile)
|
||||
|
||||
// Check that all local directories exist
|
||||
@@ -145,14 +147,14 @@ func generateHTML(yamlFile string) {
|
||||
}
|
||||
|
||||
// Write file with status
|
||||
err = writeFileWithStatus(indexPath, buf.Bytes())
|
||||
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)
|
||||
err = generateLogJSONWithStatus(logEntry, jsonPath, wantDiff, allowWrite, useColor)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to generate %s: %v", jsonPath, err)
|
||||
}
|
||||
@@ -209,9 +211,9 @@ func computeKeyInfo(logEntry *Log) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateLogJSONWithStatus(logEntry Log, outputPath string) error {
|
||||
func generateLogJSONWithStatus(logEntry Log, outputPath string, wantDiff bool, allowWrite bool, useColor bool) error {
|
||||
logJSON := LogV3JSON{
|
||||
Description: fmt.Sprintf("%s.log.ct.ipng.ch", logEntry.ShortName),
|
||||
Description: logEntry.Origin,
|
||||
SubmissionURL: fmt.Sprintf("%s/", logEntry.SubmissionPrefix),
|
||||
MonitoringURL: fmt.Sprintf("%s/", logEntry.MonitoringPrefix),
|
||||
TemporalInterval: TemporalInterval{
|
||||
@@ -228,5 +230,5 @@ func generateLogJSONWithStatus(logEntry Log, outputPath string) error {
|
||||
return fmt.Errorf("failed to marshal JSON: %v", err)
|
||||
}
|
||||
|
||||
return writeFileWithStatus(outputPath, jsonData)
|
||||
return writeFileWithStatus(outputPath, jsonData, wantDiff, allowWrite, useColor)
|
||||
}
|
||||
|
316
tesseract/genconf/html_test.go
Normal file
316
tesseract/genconf/html_test.go
Normal file
@@ -0,0 +1,316 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGenerateHTML(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create test directories
|
||||
testLogDir := filepath.Join(tmpDir, "test-log")
|
||||
err := os.MkdirAll(testLogDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate test private key
|
||||
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
privKeyDER, err := x509.MarshalECPrivateKey(privKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
privKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "EC PRIVATE KEY",
|
||||
Bytes: privKeyDER,
|
||||
})
|
||||
|
||||
keyFile := filepath.Join(tmpDir, "test-key.pem")
|
||||
err = os.WriteFile(keyFile, privKeyPEM, 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create test config
|
||||
configContent := `listen:
|
||||
- ":8080"
|
||||
logs:
|
||||
- shortname: "test-log"
|
||||
submissionprefix: "https://example.com/submit"
|
||||
monitoringprefix: "https://example.com/monitor"
|
||||
secret: "` + keyFile + `"
|
||||
localdirectory: "` + testLogDir + `"
|
||||
notafterstart: "2024-01-01T00:00:00Z"
|
||||
notafterlimit: "2025-01-01T00:00:00Z"`
|
||||
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
err = os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Run generateHTML
|
||||
generateHTML(configFile, false, true, false)
|
||||
|
||||
// Verify HTML file was created
|
||||
htmlFile := filepath.Join(testLogDir, "index.html")
|
||||
htmlContent, err := os.ReadFile(htmlFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read HTML file: %v", err)
|
||||
}
|
||||
|
||||
htmlStr := string(htmlContent)
|
||||
|
||||
// Check HTML contains expected elements
|
||||
if !strings.Contains(htmlStr, "<!DOCTYPE html>") {
|
||||
t.Error("Expected HTML doctype")
|
||||
}
|
||||
if !strings.Contains(htmlStr, "TesseraCT") {
|
||||
t.Error("Expected TesseraCT title")
|
||||
}
|
||||
if !strings.Contains(htmlStr, "example.com") {
|
||||
t.Error("Expected origin hostname in HTML")
|
||||
}
|
||||
if !strings.Contains(htmlStr, "https://example.com/submit/") {
|
||||
t.Error("Expected submission prefix in HTML")
|
||||
}
|
||||
if !strings.Contains(htmlStr, "https://example.com/monitor/") {
|
||||
t.Error("Expected monitoring prefix in HTML")
|
||||
}
|
||||
if !strings.Contains(htmlStr, "2024-01-01T00:00:00Z") {
|
||||
t.Error("Expected start time in HTML")
|
||||
}
|
||||
if !strings.Contains(htmlStr, "2025-01-01T00:00:00Z") {
|
||||
t.Error("Expected end time in HTML")
|
||||
}
|
||||
if !strings.Contains(htmlStr, "-----BEGIN PUBLIC KEY-----") {
|
||||
t.Error("Expected public key in HTML")
|
||||
}
|
||||
|
||||
// Verify JSON file was created
|
||||
jsonFile := filepath.Join(testLogDir, "log.v3.json")
|
||||
jsonContent, err := os.ReadFile(jsonFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read JSON file: %v", err)
|
||||
}
|
||||
|
||||
var logJSON LogV3JSON
|
||||
err = json.Unmarshal(jsonContent, &logJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse JSON: %v", err)
|
||||
}
|
||||
|
||||
// Verify JSON content
|
||||
if logJSON.Description != "example.com" {
|
||||
t.Errorf("Expected description 'example.com', got %s", logJSON.Description)
|
||||
}
|
||||
if logJSON.SubmissionURL != "https://example.com/submit/" {
|
||||
t.Errorf("Expected submission URL 'https://example.com/submit/', got %s", logJSON.SubmissionURL)
|
||||
}
|
||||
if logJSON.MonitoringURL != "https://example.com/monitor/" {
|
||||
t.Errorf("Expected monitoring URL 'https://example.com/monitor/', got %s", logJSON.MonitoringURL)
|
||||
}
|
||||
if logJSON.TemporalInterval.StartInclusive != "2024-01-01T00:00:00Z" {
|
||||
t.Errorf("Expected start time '2024-01-01T00:00:00Z', got %s", logJSON.TemporalInterval.StartInclusive)
|
||||
}
|
||||
if logJSON.TemporalInterval.EndExclusive != "2025-01-01T00:00:00Z" {
|
||||
t.Errorf("Expected end time '2025-01-01T00:00:00Z', got %s", logJSON.TemporalInterval.EndExclusive)
|
||||
}
|
||||
if logJSON.MMD != 60 {
|
||||
t.Errorf("Expected MMD 60, got %d", logJSON.MMD)
|
||||
}
|
||||
if logJSON.LogID == "" {
|
||||
t.Error("Expected non-empty LogID")
|
||||
}
|
||||
if logJSON.Key == "" {
|
||||
t.Error("Expected non-empty Key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeKeyInfo(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Generate test private key
|
||||
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
privKeyDER, err := x509.MarshalECPrivateKey(privKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
privKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "EC PRIVATE KEY",
|
||||
Bytes: privKeyDER,
|
||||
})
|
||||
|
||||
keyFile := filepath.Join(tmpDir, "test-key.pem")
|
||||
err = os.WriteFile(keyFile, privKeyPEM, 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create log entry
|
||||
logEntry := Log{
|
||||
ShortName: "test-log",
|
||||
Secret: keyFile,
|
||||
}
|
||||
|
||||
// Compute key info
|
||||
err = computeKeyInfo(&logEntry)
|
||||
if err != nil {
|
||||
t.Fatalf("computeKeyInfo() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify computed fields
|
||||
if logEntry.LogID == "" {
|
||||
t.Error("Expected non-empty LogID")
|
||||
}
|
||||
if logEntry.PublicKeyPEM == "" {
|
||||
t.Error("Expected non-empty PublicKeyPEM")
|
||||
}
|
||||
if !strings.Contains(logEntry.PublicKeyPEM, "-----BEGIN PUBLIC KEY-----") {
|
||||
t.Error("Expected PEM format public key")
|
||||
}
|
||||
if !strings.Contains(logEntry.PublicKeyPEM, "-----END PUBLIC KEY-----") {
|
||||
t.Error("Expected PEM format public key")
|
||||
}
|
||||
if logEntry.PublicKeyDERB64 == "" {
|
||||
t.Error("Expected non-empty PublicKeyDERB64")
|
||||
}
|
||||
if logEntry.PublicKeyBase64 == "" {
|
||||
t.Error("Expected non-empty PublicKeyBase64")
|
||||
}
|
||||
if logEntry.PublicKeyDERB64 != logEntry.PublicKeyBase64 {
|
||||
t.Error("Expected PublicKeyDERB64 and PublicKeyBase64 to be the same")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeKeyInfoInvalidKey(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create invalid key file
|
||||
invalidKeyFile := filepath.Join(tmpDir, "invalid-key.pem")
|
||||
invalidKeyContent := `-----BEGIN EC PRIVATE KEY-----
|
||||
INVALID KEY DATA
|
||||
-----END EC PRIVATE KEY-----`
|
||||
|
||||
err := os.WriteFile(invalidKeyFile, []byte(invalidKeyContent), 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
logEntry := Log{
|
||||
Secret: invalidKeyFile,
|
||||
}
|
||||
|
||||
err = computeKeyInfo(&logEntry)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid key file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeKeyInfoMissingFile(t *testing.T) {
|
||||
logEntry := Log{
|
||||
Secret: "/nonexistent/key.pem",
|
||||
}
|
||||
|
||||
err := computeKeyInfo(&logEntry)
|
||||
if err == nil {
|
||||
t.Error("Expected error for missing key file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateLogJSONWithStatus(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
outputFile := filepath.Join(tmpDir, "test-log.v3.json")
|
||||
|
||||
testTime1, _ := time.Parse("2006-01-02T15:04:05Z", "2024-01-01T00:00:00Z")
|
||||
testTime2, _ := time.Parse("2006-01-02T15:04:05Z", "2025-01-01T00:00:00Z")
|
||||
|
||||
logEntry := Log{
|
||||
Origin: "test.example.com",
|
||||
SubmissionPrefix: "https://test.example.com/submit",
|
||||
MonitoringPrefix: "https://test.example.com/monitor",
|
||||
NotAfterStart: testTime1,
|
||||
NotAfterLimit: testTime2,
|
||||
LogID: "dGVzdC1sb2ctaWQ=", // base64 encoded "test-log-id"
|
||||
PublicKeyBase64: "dGVzdC1wdWJsaWMta2V5", // base64 encoded "test-public-key"
|
||||
}
|
||||
|
||||
err := generateLogJSONWithStatus(logEntry, outputFile, false, true, false)
|
||||
if err != nil {
|
||||
t.Fatalf("generateLogJSONWithStatus() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify JSON was created
|
||||
jsonContent, err := os.ReadFile(outputFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read JSON file: %v", err)
|
||||
}
|
||||
|
||||
var logJSON LogV3JSON
|
||||
err = json.Unmarshal(jsonContent, &logJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse JSON: %v", err)
|
||||
}
|
||||
|
||||
// Verify content
|
||||
if logJSON.Description != "test.example.com" {
|
||||
t.Errorf("Expected description 'test.example.com', got %s", logJSON.Description)
|
||||
}
|
||||
if logJSON.SubmissionURL != "https://test.example.com/submit/" {
|
||||
t.Errorf("Expected submission URL to have trailing slash")
|
||||
}
|
||||
if logJSON.MonitoringURL != "https://test.example.com/monitor/" {
|
||||
t.Errorf("Expected monitoring URL to have trailing slash")
|
||||
}
|
||||
if logJSON.LogID != "dGVzdC1sb2ctaWQ=" {
|
||||
t.Errorf("Expected LogID 'dGVzdC1sb2ctaWQ=', got %s", logJSON.LogID)
|
||||
}
|
||||
if logJSON.Key != "dGVzdC1wdWJsaWMta2V5" {
|
||||
t.Errorf("Expected Key 'dGVzdC1wdWJsaWMta2V5', got %s", logJSON.Key)
|
||||
}
|
||||
if logJSON.MMD != 60 {
|
||||
t.Errorf("Expected MMD 60, got %d", logJSON.MMD)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHTMLMissingDirectory(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create config with non-existent directory
|
||||
configContent := `logs:
|
||||
- shortname: "test-log"
|
||||
submissionprefix: "https://example.com/submit"
|
||||
monitoringprefix: "https://example.com/monitor"
|
||||
secret: "test.key"
|
||||
localdirectory: "/nonexistent/directory"`
|
||||
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
err := os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Should call log.Fatalf which exits the program
|
||||
// We can't easily test this without subprocess, so we'll skip it
|
||||
t.Skip("Cannot easily test log.Fatalf without subprocess")
|
||||
}
|
@@ -12,7 +12,12 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func generateKeys(yamlFile string) {
|
||||
func generateKeys(yamlFile string, wantDiff bool, allowWrite bool, useColor bool) {
|
||||
if !allowWrite {
|
||||
fmt.Printf("Key generation requires --write flag\n")
|
||||
return
|
||||
}
|
||||
|
||||
config := loadConfig(yamlFile)
|
||||
|
||||
// Generate keys for each log
|
||||
|
296
tesseract/genconf/key_test.go
Normal file
296
tesseract/genconf/key_test.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateKeys(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create test directories
|
||||
keyDir := filepath.Join(tmpDir, "keys")
|
||||
err := os.MkdirAll(keyDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create test config
|
||||
key1Path := filepath.Join(keyDir, "test-log-1.key")
|
||||
key2Path := filepath.Join(keyDir, "test-log-2.key")
|
||||
|
||||
configContent := `logs:
|
||||
- shortname: "test-log-1"
|
||||
secret: "` + key1Path + `"
|
||||
- shortname: "test-log-2"
|
||||
secret: "` + key2Path + `"`
|
||||
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
err = os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Run generateKeys
|
||||
generateKeys(configFile, false, true, false)
|
||||
|
||||
// Verify first key was created
|
||||
if _, err := os.Stat(key1Path); os.IsNotExist(err) {
|
||||
t.Error("Expected first key file to be created")
|
||||
}
|
||||
|
||||
// Verify second key was created
|
||||
if _, err := os.Stat(key2Path); os.IsNotExist(err) {
|
||||
t.Error("Expected second key file to be created")
|
||||
}
|
||||
|
||||
// Verify key format
|
||||
keyContent, err := os.ReadFile(key1Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
keyStr := string(keyContent)
|
||||
if !strings.Contains(keyStr, "-----BEGIN EC PRIVATE KEY-----") {
|
||||
t.Error("Expected EC private key PEM header")
|
||||
}
|
||||
if !strings.Contains(keyStr, "-----END EC PRIVATE KEY-----") {
|
||||
t.Error("Expected EC private key PEM footer")
|
||||
}
|
||||
|
||||
// Verify key can be parsed
|
||||
block, _ := pem.Decode(keyContent)
|
||||
if block == nil {
|
||||
t.Error("Failed to decode PEM block")
|
||||
}
|
||||
|
||||
privKey, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to parse EC private key: %v", err)
|
||||
}
|
||||
|
||||
// Verify it's a P-256 key
|
||||
if privKey.Curve != elliptic.P256() {
|
||||
t.Error("Expected P-256 curve")
|
||||
}
|
||||
|
||||
// Verify file permissions
|
||||
info, err := os.Stat(key1Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
perm := info.Mode().Perm()
|
||||
expected := os.FileMode(0600)
|
||||
if perm != expected {
|
||||
t.Errorf("Expected file mode %o, got %o", expected, perm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateKeysExistingKey(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create existing key file
|
||||
keyPath := filepath.Join(tmpDir, "existing.key")
|
||||
existingContent := `-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIExistingKeyContent
|
||||
-----END EC PRIVATE KEY-----`
|
||||
|
||||
err := os.WriteFile(keyPath, []byte(existingContent), 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create test config
|
||||
configContent := `logs:
|
||||
- shortname: "test-log"
|
||||
secret: "` + keyPath + `"`
|
||||
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
err = os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Run generateKeys
|
||||
generateKeys(configFile, false, true, false)
|
||||
|
||||
// Verify existing key was not overwritten
|
||||
keyContent, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(keyContent) != existingContent {
|
||||
t.Error("Expected existing key to be preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateKeysWithoutWriteFlag(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
keyPath := filepath.Join(tmpDir, "test.key")
|
||||
|
||||
configContent := `logs:
|
||||
- shortname: "test-log"
|
||||
secret: "` + keyPath + `"`
|
||||
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
err := os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Run generateKeys without write flag
|
||||
generateKeys(configFile, false, false, false)
|
||||
|
||||
// Verify key was not created
|
||||
if _, err := os.Stat(keyPath); !os.IsNotExist(err) {
|
||||
t.Error("Expected key file to not be created without --write flag")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateKeysCreateDirectory(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Key path with non-existent directory
|
||||
keyDir := filepath.Join(tmpDir, "subdir", "keys")
|
||||
keyPath := filepath.Join(keyDir, "test.key")
|
||||
|
||||
configContent := `logs:
|
||||
- shortname: "test-log"
|
||||
secret: "` + keyPath + `"`
|
||||
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
err := os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Run generateKeys
|
||||
generateKeys(configFile, false, true, false)
|
||||
|
||||
// Verify directory was created
|
||||
if _, err := os.Stat(keyDir); os.IsNotExist(err) {
|
||||
t.Error("Expected directory to be created")
|
||||
}
|
||||
|
||||
// Verify key was created
|
||||
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
|
||||
t.Error("Expected key file to be created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateKeysMultipleRuns(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
key1Path := filepath.Join(tmpDir, "key1.key")
|
||||
key2Path := filepath.Join(tmpDir, "key2.key")
|
||||
|
||||
configContent := `logs:
|
||||
- shortname: "test-log-1"
|
||||
secret: "` + key1Path + `"
|
||||
- shortname: "test-log-2"
|
||||
secret: "` + key2Path + `"`
|
||||
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
err := os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// First run - should create both keys
|
||||
generateKeys(configFile, false, true, false)
|
||||
|
||||
// Read first key content
|
||||
key1Content, err := os.ReadFile(key1Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
key2Content, err := os.ReadFile(key2Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Second run - should not overwrite existing keys
|
||||
generateKeys(configFile, false, true, false)
|
||||
|
||||
// Verify keys are unchanged
|
||||
key1Content2, err := os.ReadFile(key1Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
key2Content2, err := os.ReadFile(key2Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(key1Content) != string(key1Content2) {
|
||||
t.Error("First key should not have been overwritten")
|
||||
}
|
||||
|
||||
if string(key2Content) != string(key2Content2) {
|
||||
t.Error("Second key should not have been overwritten")
|
||||
}
|
||||
}
|
||||
|
||||
func TestECKeyGeneration(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
keyPath := filepath.Join(tmpDir, "test.key")
|
||||
|
||||
configContent := `logs:
|
||||
- shortname: "test-log"
|
||||
secret: "` + keyPath + `"`
|
||||
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
err := os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
generateKeys(configFile, false, true, false)
|
||||
|
||||
// Read and parse the generated key
|
||||
keyContent, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(keyContent)
|
||||
if block == nil {
|
||||
t.Fatal("Failed to decode PEM block")
|
||||
}
|
||||
|
||||
if block.Type != "EC PRIVATE KEY" {
|
||||
t.Errorf("Expected block type 'EC PRIVATE KEY', got %s", block.Type)
|
||||
}
|
||||
|
||||
privKey, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse EC private key: %v", err)
|
||||
}
|
||||
|
||||
// Verify public key can be derived
|
||||
pubKey := &privKey.PublicKey
|
||||
if pubKey.X == nil || pubKey.Y == nil {
|
||||
t.Error("Public key coordinates should not be nil")
|
||||
}
|
||||
|
||||
// Verify key is on the correct curve
|
||||
if !pubKey.Curve.IsOnCurve(pubKey.X, pubKey.Y) {
|
||||
t.Error("Public key is not on the curve")
|
||||
}
|
||||
|
||||
// Verify we can marshal the public key (for log ID computation)
|
||||
_, err = x509.MarshalPKIXPublicKey(pubKey)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to marshal public key: %v", err)
|
||||
}
|
||||
}
|
@@ -5,11 +5,16 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hexops/gotextdiff"
|
||||
"github.com/hexops/gotextdiff/myers"
|
||||
"github.com/hexops/gotextdiff/span"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config represents the main YAML configuration structure for tesseract
|
||||
type Config struct {
|
||||
Listen []string `yaml:"listen"`
|
||||
Checkpoints string `yaml:"checkpoints"`
|
||||
@@ -17,6 +22,7 @@ type Config struct {
|
||||
Logs []Log `yaml:"logs"`
|
||||
}
|
||||
|
||||
// Log represents a single Certificate Transparency log configuration
|
||||
type Log struct {
|
||||
ShortName string `yaml:"shortname"`
|
||||
Inception string `yaml:"inception"`
|
||||
@@ -37,10 +43,14 @@ type Log struct {
|
||||
PublicKeyPEM string
|
||||
PublicKeyDERB64 string
|
||||
PublicKeyBase64 string
|
||||
Origin string
|
||||
}
|
||||
|
||||
func main() {
|
||||
configFile := flag.String("c", "./tesseract-staging.yaml", "Path to the YAML configuration file")
|
||||
wantDiff := flag.Bool("diff", false, "Show unified diff of changes")
|
||||
allowWrite := flag.Bool("write", false, "Allow writing files (required for actual file modifications)")
|
||||
noColor := flag.Bool("no-color", false, "Disable colored diff output")
|
||||
flag.Parse()
|
||||
|
||||
args := flag.Args()
|
||||
@@ -51,15 +61,15 @@ func main() {
|
||||
|
||||
switch args[0] {
|
||||
case "gen-html":
|
||||
generateHTML(*configFile)
|
||||
generateHTML(*configFile, *wantDiff, *allowWrite, !*noColor)
|
||||
case "gen-env":
|
||||
generateEnv(*configFile)
|
||||
generateEnv(*configFile, *wantDiff, *allowWrite, !*noColor)
|
||||
case "gen-key":
|
||||
generateKeys(*configFile)
|
||||
generateKeys(*configFile, *wantDiff, *allowWrite, !*noColor)
|
||||
case "gen-nginx":
|
||||
generateNginx(*configFile)
|
||||
generateNginx(*configFile, *wantDiff, *allowWrite, !*noColor)
|
||||
case "gen-roots":
|
||||
generateRoots(args[1:])
|
||||
generateRoots(args[1:], *wantDiff, *allowWrite, !*noColor)
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", args[0])
|
||||
showHelp()
|
||||
@@ -92,22 +102,108 @@ func loadConfig(yamlFile string) Config {
|
||||
if config.Logs[i].Period == 0 {
|
||||
config.Logs[i].Period = 200
|
||||
}
|
||||
|
||||
// Extract hostname from SubmissionPrefix to set Origin
|
||||
if config.Logs[i].SubmissionPrefix != "" {
|
||||
hostname, err := extractHostname(config.Logs[i].SubmissionPrefix)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse SubmissionPrefix URL for %s: %v", config.Logs[i].ShortName, err)
|
||||
}
|
||||
config.Logs[i].Origin = hostname
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func writeFileWithStatus(filename string, content []byte) error {
|
||||
// ANSI color codes
|
||||
const (
|
||||
colorReset = "\033[0m"
|
||||
colorRed = "\033[31m"
|
||||
colorGreen = "\033[32m"
|
||||
colorYellow = "\033[33m"
|
||||
colorCyan = "\033[36m"
|
||||
)
|
||||
|
||||
// colorizeUnifiedDiff adds ANSI color codes to unified diff output
|
||||
func colorizeUnifiedDiff(diff string) string {
|
||||
lines := strings.Split(diff, "\n")
|
||||
var colorizedLines []string
|
||||
|
||||
for _, line := range lines {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "---"):
|
||||
// File deletion header in cyan
|
||||
colorizedLines = append(colorizedLines, colorCyan+line+colorReset)
|
||||
case strings.HasPrefix(line, "+++"):
|
||||
// File addition header in cyan
|
||||
colorizedLines = append(colorizedLines, colorCyan+line+colorReset)
|
||||
case strings.HasPrefix(line, "@@"):
|
||||
// Hunk header in yellow
|
||||
colorizedLines = append(colorizedLines, colorYellow+line+colorReset)
|
||||
case strings.HasPrefix(line, "-"):
|
||||
// Deleted lines in red
|
||||
colorizedLines = append(colorizedLines, colorRed+line+colorReset)
|
||||
case strings.HasPrefix(line, "+"):
|
||||
// Added lines in green
|
||||
colorizedLines = append(colorizedLines, colorGreen+line+colorReset)
|
||||
default:
|
||||
// Context lines unchanged
|
||||
colorizedLines = append(colorizedLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(colorizedLines, "\n")
|
||||
}
|
||||
|
||||
func writeFileWithStatus(filename string, content []byte, wantDiff bool, allowWrite bool, useColor bool) error {
|
||||
existingContent, err := os.ReadFile(filename)
|
||||
if os.IsNotExist(err) {
|
||||
fmt.Printf("Creating %s\n", filename)
|
||||
isNew := os.IsNotExist(err)
|
||||
isUnchanged := false
|
||||
|
||||
if isNew {
|
||||
if allowWrite {
|
||||
fmt.Printf("Creating %s\n", filename)
|
||||
} else {
|
||||
fmt.Printf("Would create %s\n", filename)
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to read existing file %s: %v", filename, err)
|
||||
} else if string(existingContent) == string(content) {
|
||||
fmt.Printf("Unchanged %s\n", filename)
|
||||
return nil
|
||||
isUnchanged = true
|
||||
} else {
|
||||
fmt.Printf("Updating %s\n", filename)
|
||||
if allowWrite {
|
||||
fmt.Printf("Updating %s\n", filename)
|
||||
} else {
|
||||
fmt.Printf("Would update %s\n", filename)
|
||||
}
|
||||
}
|
||||
|
||||
if wantDiff && !isUnchanged {
|
||||
if isNew {
|
||||
// For new files, show the entire content as added
|
||||
edits := myers.ComputeEdits(span.URIFromPath(filename), "", string(content))
|
||||
diff := fmt.Sprint(gotextdiff.ToUnified("/dev/null", filename, "", edits))
|
||||
if useColor {
|
||||
fmt.Print(colorizeUnifiedDiff(diff))
|
||||
} else {
|
||||
fmt.Print(diff)
|
||||
}
|
||||
} else {
|
||||
// For existing files, show the diff
|
||||
edits := myers.ComputeEdits(span.URIFromPath(filename), string(existingContent), string(content))
|
||||
diff := fmt.Sprint(gotextdiff.ToUnified(filename, filename+".new", string(existingContent), edits))
|
||||
if useColor {
|
||||
fmt.Print(colorizeUnifiedDiff(diff))
|
||||
} else {
|
||||
fmt.Print(diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isUnchanged || !allowWrite {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = os.WriteFile(filename, content, 0644)
|
||||
@@ -119,8 +215,17 @@ func writeFileWithStatus(filename string, content []byte) error {
|
||||
|
||||
func showHelp() {
|
||||
fmt.Printf("Usage: %s [options] <command>\n\n", os.Args[0])
|
||||
fmt.Printf("Note: Flags must come before the command name.\n\n")
|
||||
fmt.Printf("Options:\n")
|
||||
fmt.Printf(" -c <file> Path to YAML configuration file (default: ./tesseract-staging.yaml)\n\n")
|
||||
fmt.Printf(" -c <file> Path to YAML configuration file (default: ./tesseract-staging.yaml)\n")
|
||||
fmt.Printf(" --diff Show unified diff of changes without writing files\n")
|
||||
fmt.Printf(" --write Allow writing files (required for actual file modifications)\n")
|
||||
fmt.Printf(" --no-color Disable colored diff output\n\n")
|
||||
fmt.Printf("Examples:\n")
|
||||
fmt.Printf(" %s --diff gen-html # Show colored diffs without writing\n", os.Args[0])
|
||||
fmt.Printf(" %s --diff --no-color gen-html # Show plain diffs without writing\n", os.Args[0])
|
||||
fmt.Printf(" %s --write gen-html # Write files\n", os.Args[0])
|
||||
fmt.Printf(" %s --diff --write gen-html # Show colored diffs and write files\n\n", os.Args[0])
|
||||
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")
|
||||
|
273
tesseract/genconf/main_test.go
Normal file
273
tesseract/genconf/main_test.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
yamlContent string
|
||||
wantError bool
|
||||
checkFields func(*testing.T, Config)
|
||||
}{
|
||||
{
|
||||
name: "valid config with all fields",
|
||||
yamlContent: `listen:
|
||||
- ":8080"
|
||||
checkpoints: "checkpoints"
|
||||
roots: "roots.pem"
|
||||
logs:
|
||||
- shortname: "test-log"
|
||||
inception: "2024-01-01T00:00:00Z"
|
||||
period: 300
|
||||
poolsize: 1000
|
||||
submissionprefix: "https://example.com/submit"
|
||||
monitoringprefix: "https://example.com/monitor"
|
||||
secret: "test.key"
|
||||
localdirectory: "test-dir"
|
||||
notafterstart: "2024-01-01T00:00:00Z"
|
||||
notafterlimit: "2025-01-01T00:00:00Z"`,
|
||||
wantError: false,
|
||||
checkFields: func(t *testing.T, config Config) {
|
||||
if len(config.Listen) != 1 || config.Listen[0] != ":8080" {
|
||||
t.Errorf("Expected Listen [\":8080\"], got %v", config.Listen)
|
||||
}
|
||||
if config.Checkpoints != "checkpoints" {
|
||||
t.Errorf("Expected Checkpoints \"checkpoints\", got %s", config.Checkpoints)
|
||||
}
|
||||
if len(config.Logs) != 1 {
|
||||
t.Errorf("Expected 1 log, got %d", len(config.Logs))
|
||||
}
|
||||
log := config.Logs[0]
|
||||
if log.ShortName != "test-log" {
|
||||
t.Errorf("Expected ShortName \"test-log\", got %s", log.ShortName)
|
||||
}
|
||||
if log.Period != 300 {
|
||||
t.Errorf("Expected Period 300, got %d", log.Period)
|
||||
}
|
||||
if log.PoolSize != 1000 {
|
||||
t.Errorf("Expected PoolSize 1000, got %d", log.PoolSize)
|
||||
}
|
||||
if log.Origin != "example.com" {
|
||||
t.Errorf("Expected Origin \"example.com\", got %s", log.Origin)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config with defaults",
|
||||
yamlContent: `logs:
|
||||
- shortname: "minimal-log"
|
||||
submissionprefix: "https://test.example.com/submit"
|
||||
secret: "test.key"
|
||||
localdirectory: "test-dir"`,
|
||||
wantError: false,
|
||||
checkFields: func(t *testing.T, config Config) {
|
||||
if len(config.Listen) != 1 || config.Listen[0] != ":8080" {
|
||||
t.Errorf("Expected default Listen [\":8080\"], got %v", config.Listen)
|
||||
}
|
||||
if len(config.Logs) != 1 {
|
||||
t.Errorf("Expected 1 log, got %d", len(config.Logs))
|
||||
}
|
||||
log := config.Logs[0]
|
||||
if log.Period != 200 {
|
||||
t.Errorf("Expected default Period 200, got %d", log.Period)
|
||||
}
|
||||
if log.PoolSize != 750 {
|
||||
t.Errorf("Expected default PoolSize 750, got %d", log.PoolSize)
|
||||
}
|
||||
if log.Origin != "test.example.com" {
|
||||
t.Errorf("Expected Origin \"test.example.com\", got %s", log.Origin)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid yaml",
|
||||
yamlContent: `invalid: yaml: content:
|
||||
- malformed`,
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpfile, err := os.CreateTemp("", "test-config-*.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
_, err = tmpfile.WriteString(tt.yamlContent)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpfile.Close()
|
||||
|
||||
if tt.wantError {
|
||||
// Can't easily test log.Fatalf without subprocess
|
||||
t.Skip("Cannot easily test log.Fatalf without subprocess")
|
||||
} else {
|
||||
config := loadConfig(tmpfile.Name())
|
||||
if tt.checkFields != nil {
|
||||
tt.checkFields(t, config)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractHostname(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
urlStr string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "https URL",
|
||||
urlStr: "https://example.com/path",
|
||||
want: "example.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "http URL",
|
||||
urlStr: "http://test.example.com:8080/path",
|
||||
want: "test.example.com:8080",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "URL with port",
|
||||
urlStr: "https://log.example.com:9090/monitor",
|
||||
want: "log.example.com:9090",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid URL",
|
||||
urlStr: "://invalid-url",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := extractHostname(tt.urlStr)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("extractHostname() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("extractHostname() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractPort(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
listenAddr string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "port only",
|
||||
listenAddr: ":8080",
|
||||
want: "8080",
|
||||
},
|
||||
{
|
||||
name: "localhost with port",
|
||||
listenAddr: "localhost:9090",
|
||||
want: "9090",
|
||||
},
|
||||
{
|
||||
name: "IPv6 with port",
|
||||
listenAddr: "[::]:8080",
|
||||
want: "8080",
|
||||
},
|
||||
{
|
||||
name: "no port",
|
||||
listenAddr: "localhost",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
listenAddr: "",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := extractPort(tt.listenAddr)
|
||||
if got != tt.want {
|
||||
t.Errorf("extractPort() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorizeUnifiedDiff(t *testing.T) {
|
||||
diff := `--- file1.txt
|
||||
+++ file2.txt
|
||||
@@ -1,3 +1,3 @@
|
||||
line1
|
||||
-old line
|
||||
+new line
|
||||
line3`
|
||||
|
||||
result := colorizeUnifiedDiff(diff)
|
||||
|
||||
if !strings.Contains(result, colorCyan) {
|
||||
t.Error("Expected diff to contain cyan color codes")
|
||||
}
|
||||
if !strings.Contains(result, colorYellow) {
|
||||
t.Error("Expected diff to contain yellow color codes")
|
||||
}
|
||||
if !strings.Contains(result, colorRed) {
|
||||
t.Error("Expected diff to contain red color codes")
|
||||
}
|
||||
if !strings.Contains(result, colorGreen) {
|
||||
t.Error("Expected diff to contain green color codes")
|
||||
}
|
||||
if !strings.Contains(result, colorReset) {
|
||||
t.Error("Expected diff to contain reset color codes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteFileWithStatus(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := tmpDir + "/test.txt"
|
||||
content := []byte("test content")
|
||||
|
||||
err := writeFileWithStatus(testFile, content, false, true, false)
|
||||
if err != nil {
|
||||
t.Errorf("writeFileWithStatus() error = %v", err)
|
||||
}
|
||||
|
||||
writtenContent, err := os.ReadFile(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read written file: %v", err)
|
||||
}
|
||||
|
||||
if string(writtenContent) != string(content) {
|
||||
t.Errorf("Written content = %s, want %s", string(writtenContent), string(content))
|
||||
}
|
||||
|
||||
err = writeFileWithStatus(testFile, content, false, false, false)
|
||||
if err != nil {
|
||||
t.Errorf("writeFileWithStatus() with allowWrite=false should not error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeFormats(t *testing.T) {
|
||||
testTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
||||
expected := "2024-01-01T12:00:00Z"
|
||||
|
||||
formatted := testTime.Format("2006-01-02T15:04:05Z")
|
||||
if formatted != expected {
|
||||
t.Errorf("Time format = %s, want %s", formatted, expected)
|
||||
}
|
||||
}
|
@@ -70,13 +70,14 @@ const nginxTemplate = `server {
|
||||
}
|
||||
`
|
||||
|
||||
// NginxTemplateData contains the data needed to generate nginx configuration files
|
||||
type NginxTemplateData struct {
|
||||
MonitoringHost string
|
||||
LocalDirectory string
|
||||
ListenPort string
|
||||
}
|
||||
|
||||
func generateNginx(yamlFile string) {
|
||||
func generateNginx(yamlFile string, wantDiff bool, allowWrite bool, useColor bool) {
|
||||
config := loadConfig(yamlFile)
|
||||
|
||||
// Extract port from first listen address
|
||||
@@ -123,7 +124,7 @@ func generateNginx(yamlFile string) {
|
||||
}
|
||||
|
||||
// Write file with status
|
||||
err = writeFileWithStatus(outputPath, buf.Bytes())
|
||||
err = writeFileWithStatus(outputPath, buf.Bytes(), wantDiff, allowWrite, useColor)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to write nginx config file %s: %v\n", outputPath, err)
|
||||
continue
|
||||
@@ -151,14 +152,10 @@ func extractPort(listenAddr string) string {
|
||||
}
|
||||
|
||||
func extractHostname(urlStr string) (string, error) {
|
||||
if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") {
|
||||
urlStr = "https://" + urlStr
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return parsedURL.Hostname(), nil
|
||||
return parsedURL.Host, nil
|
||||
}
|
||||
|
307
tesseract/genconf/nginx_test.go
Normal file
307
tesseract/genconf/nginx_test.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateNginx(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create test directories
|
||||
log1Dir := filepath.Join(tmpDir, "log1")
|
||||
log2Dir := filepath.Join(tmpDir, "log2")
|
||||
err := os.MkdirAll(log1Dir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = os.MkdirAll(log2Dir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create test config
|
||||
configContent := `listen:
|
||||
- ":8080"
|
||||
logs:
|
||||
- shortname: "log1"
|
||||
monitoringprefix: "https://log1.example.com/monitor"
|
||||
localdirectory: "` + log1Dir + `"
|
||||
- shortname: "log2"
|
||||
monitoringprefix: "https://log2.example.com:9090/monitor"
|
||||
localdirectory: "` + log2Dir + `"`
|
||||
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
err = os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Run generateNginx
|
||||
generateNginx(configFile, false, true, false)
|
||||
|
||||
// Verify nginx config for first log
|
||||
nginxFile1 := filepath.Join(log1Dir, "log1.example.com.conf")
|
||||
nginxContent1, err := os.ReadFile(nginxFile1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read nginx config: %v", err)
|
||||
}
|
||||
|
||||
nginx1Str := string(nginxContent1)
|
||||
|
||||
// Check server block basics
|
||||
if !strings.Contains(nginx1Str, "server {") {
|
||||
t.Error("Expected server block")
|
||||
}
|
||||
if !strings.Contains(nginx1Str, "listen 8080;") {
|
||||
t.Error("Expected listen directive with port 8080")
|
||||
}
|
||||
if !strings.Contains(nginx1Str, "listen [::]:8080;") {
|
||||
t.Error("Expected IPv6 listen directive")
|
||||
}
|
||||
if !strings.Contains(nginx1Str, "server_name log1.example.com;") {
|
||||
t.Error("Expected correct server name")
|
||||
}
|
||||
if !strings.Contains(nginx1Str, "root "+log1Dir+";") {
|
||||
t.Error("Expected correct document root")
|
||||
}
|
||||
|
||||
// Check location blocks
|
||||
if !strings.Contains(nginx1Str, "location = / {") {
|
||||
t.Error("Expected root location block")
|
||||
}
|
||||
if !strings.Contains(nginx1Str, "location = /checkpoint {") {
|
||||
t.Error("Expected checkpoint location block")
|
||||
}
|
||||
if !strings.Contains(nginx1Str, "location = /log.v3.json {") {
|
||||
t.Error("Expected log.v3.json location block")
|
||||
}
|
||||
if !strings.Contains(nginx1Str, "location ~ ^/issuer/(.+)$ {") {
|
||||
t.Error("Expected issuer location block")
|
||||
}
|
||||
if !strings.Contains(nginx1Str, "location ~ ^/tile/(.+)$ {") {
|
||||
t.Error("Expected tile location block")
|
||||
}
|
||||
|
||||
// Check CORS headers
|
||||
if !strings.Contains(nginx1Str, `add_header Access-Control-Allow-Origin "*" always;`) {
|
||||
t.Error("Expected CORS headers")
|
||||
}
|
||||
|
||||
// Check cache control headers
|
||||
if !strings.Contains(nginx1Str, `add_header Cache-Control "no-store" always;`) {
|
||||
t.Error("Expected no-store cache control for checkpoint")
|
||||
}
|
||||
if !strings.Contains(nginx1Str, `add_header Cache-Control "public, max-age=3600, immutable" always;`) {
|
||||
t.Error("Expected long cache control for log.v3.json")
|
||||
}
|
||||
if !strings.Contains(nginx1Str, `add_header Cache-Control "public, max-age=604800, immutable" always;`) {
|
||||
t.Error("Expected very long cache control for static files")
|
||||
}
|
||||
|
||||
// Check content types
|
||||
if !strings.Contains(nginx1Str, `add_header Content-Type "text/html; charset=utf-8" always;`) {
|
||||
t.Error("Expected HTML content type")
|
||||
}
|
||||
if !strings.Contains(nginx1Str, `add_header Content-Type "text/plain; charset=utf-8" always;`) {
|
||||
t.Error("Expected plain text content type for checkpoint")
|
||||
}
|
||||
if !strings.Contains(nginx1Str, `add_header Content-Type "application/json" always;`) {
|
||||
t.Error("Expected JSON content type")
|
||||
}
|
||||
if !strings.Contains(nginx1Str, `add_header Content-Type "application/pkix-cert" always;`) {
|
||||
t.Error("Expected certificate content type")
|
||||
}
|
||||
if !strings.Contains(nginx1Str, `add_header Content-Type "application/octet-stream" always;`) {
|
||||
t.Error("Expected octet-stream content type")
|
||||
}
|
||||
|
||||
// Check gzip handling
|
||||
if !strings.Contains(nginx1Str, `add_header Content-Encoding "gzip" always;`) {
|
||||
t.Error("Expected gzip content encoding for .gz files")
|
||||
}
|
||||
|
||||
// Verify nginx config for second log (with port)
|
||||
nginxFile2 := filepath.Join(log2Dir, "log2.example.com:9090.conf")
|
||||
nginxContent2, err := os.ReadFile(nginxFile2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read second nginx config: %v", err)
|
||||
}
|
||||
|
||||
nginx2Str := string(nginxContent2)
|
||||
if !strings.Contains(nginx2Str, "server_name log2.example.com:9090;") {
|
||||
t.Error("Expected correct server name with port")
|
||||
}
|
||||
if !strings.Contains(nginx2Str, "root "+log2Dir+";") {
|
||||
t.Error("Expected correct document root for second log")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateNginxCustomPort(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
logDir := filepath.Join(tmpDir, "test-log")
|
||||
err := os.MkdirAll(logDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create config with custom port
|
||||
configContent := `listen:
|
||||
- ":9999"
|
||||
logs:
|
||||
- shortname: "test-log"
|
||||
monitoringprefix: "https://test.example.com/monitor"
|
||||
localdirectory: "` + logDir + `"`
|
||||
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
err = os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
generateNginx(configFile, false, true, false)
|
||||
|
||||
nginxFile := filepath.Join(logDir, "test.example.com.conf")
|
||||
nginxContent, err := os.ReadFile(nginxFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read nginx config: %v", err)
|
||||
}
|
||||
|
||||
nginxStr := string(nginxContent)
|
||||
if !strings.Contains(nginxStr, "listen 9999;") {
|
||||
t.Error("Expected listen directive with custom port 9999")
|
||||
}
|
||||
if !strings.Contains(nginxStr, "listen [::]:9999;") {
|
||||
t.Error("Expected IPv6 listen directive with custom port")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateNginxMultiplePorts(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
logDir := filepath.Join(tmpDir, "test-log")
|
||||
err := os.MkdirAll(logDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create config with multiple listen addresses (should use first one)
|
||||
configContent := `listen:
|
||||
- ":8080"
|
||||
- ":8081"
|
||||
logs:
|
||||
- shortname: "test-log"
|
||||
monitoringprefix: "https://test.example.com/monitor"
|
||||
localdirectory: "` + logDir + `"`
|
||||
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
err = os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
generateNginx(configFile, false, true, false)
|
||||
|
||||
nginxFile := filepath.Join(logDir, "test.example.com.conf")
|
||||
nginxContent, err := os.ReadFile(nginxFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read nginx config: %v", err)
|
||||
}
|
||||
|
||||
nginxStr := string(nginxContent)
|
||||
if !strings.Contains(nginxStr, "listen 8080;") {
|
||||
t.Error("Expected first port to be used")
|
||||
}
|
||||
if strings.Contains(nginxStr, "listen 8081;") {
|
||||
t.Error("Should not contain second port")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateNginxInvalidURL(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
logDir := filepath.Join(tmpDir, "test-log")
|
||||
err := os.MkdirAll(logDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create config with invalid monitoring URL
|
||||
configContent := `logs:
|
||||
- shortname: "test-log"
|
||||
monitoringprefix: "not-a-valid-url"
|
||||
localdirectory: "` + logDir + `"`
|
||||
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
err = os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Should not create nginx config file due to invalid URL
|
||||
generateNginx(configFile, false, true, false)
|
||||
|
||||
// Check that a .conf file was still created (even with invalid hostname extraction)
|
||||
// The function continues execution and creates a file with empty hostname
|
||||
files, err := filepath.Glob(filepath.Join(logDir, "*.conf"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// The function still creates a file, but with an empty hostname, resulting in ".conf"
|
||||
if len(files) == 0 {
|
||||
t.Error("Expected at least one config file to be created despite invalid URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxTemplateData(t *testing.T) {
|
||||
data := NginxTemplateData{
|
||||
MonitoringHost: "example.com",
|
||||
LocalDirectory: "/path/to/files",
|
||||
ListenPort: "8080",
|
||||
}
|
||||
|
||||
if data.MonitoringHost != "example.com" {
|
||||
t.Errorf("Expected MonitoringHost 'example.com', got %s", data.MonitoringHost)
|
||||
}
|
||||
if data.LocalDirectory != "/path/to/files" {
|
||||
t.Errorf("Expected LocalDirectory '/path/to/files', got %s", data.LocalDirectory)
|
||||
}
|
||||
if data.ListenPort != "8080" {
|
||||
t.Errorf("Expected ListenPort '8080', got %s", data.ListenPort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateNginxNoListenConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
logDir := filepath.Join(tmpDir, "test-log")
|
||||
err := os.MkdirAll(logDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create config without listen directive (should use default 8080)
|
||||
configContent := `logs:
|
||||
- shortname: "test-log"
|
||||
monitoringprefix: "https://test.example.com/monitor"
|
||||
localdirectory: "` + logDir + `"`
|
||||
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
err = os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
generateNginx(configFile, false, true, false)
|
||||
|
||||
nginxFile := filepath.Join(logDir, "test.example.com.conf")
|
||||
nginxContent, err := os.ReadFile(nginxFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read nginx config: %v", err)
|
||||
}
|
||||
|
||||
nginxStr := string(nginxContent)
|
||||
if !strings.Contains(nginxStr, "listen 8080;") {
|
||||
t.Error("Expected default port 8080 when no listen config provided")
|
||||
}
|
||||
}
|
@@ -12,11 +12,12 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CTLogRootsResponse represents the JSON response from a CT log's get-roots endpoint
|
||||
type CTLogRootsResponse struct {
|
||||
Certificates []string `json:"certificates"`
|
||||
}
|
||||
|
||||
func generateRoots(args []string) {
|
||||
func generateRoots(args []string, wantDiff bool, allowWrite bool, useColor bool) {
|
||||
sourceURL := "https://rennet2027h2.log.ct.ipng.ch/"
|
||||
outputFile := "roots.pem"
|
||||
|
||||
@@ -107,7 +108,7 @@ func generateRoots(args []string) {
|
||||
}
|
||||
|
||||
// Write all certificates to file with status
|
||||
err = writeFileWithStatus(outputFile, pemBuffer.Bytes())
|
||||
err = writeFileWithStatus(outputFile, pemBuffer.Bytes(), wantDiff, allowWrite, useColor)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to write output file %s: %v", outputFile, err)
|
||||
}
|
||||
|
217
tesseract/genconf/roots_test.go
Normal file
217
tesseract/genconf/roots_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateRoots(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
outputFile := filepath.Join(tmpDir, "test-roots.pem")
|
||||
|
||||
// Create a test HTTP server that returns mock root certificates
|
||||
mockResponse := CTLogRootsResponse{
|
||||
Certificates: []string{
|
||||
// Real certificate (base64 encoded DER from OpenSSL)
|
||||
"MIIDFzCCAf+gAwIBAgIUbOZ5dIVBuGei8T0VdIKjCpACVgMwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw0yNTA4MjgxOTA4MjlaFw0yNjA4MjgxOTA4MjlaMBsxGTAXBgNVBAMMEFRlc3QgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEmF+JcVMD9u0efdi8AkIH4xiKb/8lJveOYu5UKJ/hE7j+QN0HcK+3d0Fh83Hgny7ZgF3A8JlTDnlZrkI1+n8uxLAbvZ/PVNS286FwlnWppnarSBwFMuHp+SoGFuNIQtB5LaxVt943Oxaw4vVaFBqi0iYPFKXj2JxLIwiwKxLiZ2hS15g/JIhRXSycHjMCvukPj1wtRo/cHF83Miydw+ngFqrfXcEzvG/Yx5qvAgowvG/FEKha5zB36ywy35SHq8gX7DHs77eQ52lxd4yvYfPhIwaAzTvnpyACcvfgP/XiETMxsaPoZ3WFzK4y3Smg6zUMLAB1YE4RxUd+4NA+NBmbAgMBAAGjUzBRMB0GA1UdDgQWBBQxgJBg/4o2f1e58aBMOlyEO0svIDAfBgNVHSMEGDAWgBQxgJBg/4o2f1e58aBMOlyEO0svIDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA1Z1r8pJeYizgo30kA+Q4SICq70jvAO6XPrin2hp8MlvE7nUG2afInzNh6FXjqMI0TWctqV+wF9nnCkPBPu4ybCbm7reiJ1SojPmp/qxv+qoyXXFZxX/ugOhhJv4BbSoHxepdSPBtqQb5uZPL/1q9vYmLfsEdEdh1dI4vEbrvvZR/fV/5+ZjL2uOuWf6zw0BpMqXyC0wcoZIWKHl62yLOhdJwbUVLfKiHuwrhsdIIc0QDI74U6x5pu/eIa1hCPX9e/X8vEK8/0EBV2xGhAeeteu4bY04AjrSs0tESQO5EfACmhQM/1ytlqMx3qsPGb7pIptUttHREvr+RL6qKCRZCK",
|
||||
// Second certificate (same but different serial for testing)
|
||||
"MIIDFzCCAf+gAwIBAgIUbOZ5dIVBuGei8T0VdIKjCpACVgQwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw0yNTA4MjgxOTA4MjlaFw0yNjA4MjgxOTA4MjlaMBsxGTAXBgNVBAMMEFRlc3QgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEmF+JcVMD9u0efdi8AkIH4xiKb/8lJveOYu5UKJ/hE7j+QN0HcK+3d0Fh83Hgny7ZgF3A8JlTDnlZrkI1+n8uxLAbvZ/PVNS286FwlnWppnarSBwFMuHp+SoGFuNIQtB5LaxVt943Oxaw4vVaFBqi0iYPFKXj2JxLIwiwKxLiZ2hS15g/JIhRXSycHjMCvukPj1wtRo/cHF83Miydw+ngFqrfXcEzvG/Yx5qvAgowvG/FEKha5zB36ywy35SHq8gX7DHs77eQ52lxd4yvYfPhIwaAzTvnpyACcvfgP/XiETMxsaPoZ3WFzK4y3Smg6zUMLAB1YE4RxUd+4NA+NBmbAgMBAAGjUzBRMB0GA1UdDgQWBBQxgJBg/4o2f1e58aBMOlyEO0svIDAfBgNVHSMEGDAWgBQxgJBg/4o2f1e58aBMOlyEO0svIDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA1Z1r8pJeYizgo30kA+Q4SICq70jvAO6XPrin2hp8MlvE7nUG2afInzNh6FXjqMI0TWctqV+wF9nnCkPBPu4ybCbm7reiJ1SojPmp/qxv+qoyXXFZxX/ugOhhJv4BbSoHxepdSPBtqQb5uZPL/1q9vYmLfsEdEdh1dI4vEbrvvZR/fV/5+ZjL2uOuWf6zw0BpMqXyC0wcoZIWKHl62yLOhdJwbUVLfKiHuwrhsdIIc0QDI74U6x5pu/eIa1hCPX9e/X8vEK8/0EBV2xGhAeeteu4bY04AjrSs0tESQO5EfACmhQM/1ytlqMx3qsPGb7pIptUttHREvr+RL6qKCRZCK",
|
||||
},
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasSuffix(r.URL.Path, "/ct/v1/get-roots") {
|
||||
t.Errorf("Expected path to end with /ct/v1/get-roots, got %s", r.URL.Path)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(mockResponse)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Test with default arguments
|
||||
args := []string{"--source", server.URL, "--output", outputFile}
|
||||
generateRoots(args, false, true, false)
|
||||
|
||||
// Verify output file was created
|
||||
if _, err := os.Stat(outputFile); os.IsNotExist(err) {
|
||||
t.Fatal("Output file was not created")
|
||||
}
|
||||
|
||||
// Verify file contents
|
||||
content, err := os.ReadFile(outputFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
contentStr := string(content)
|
||||
|
||||
// Should contain PEM headers
|
||||
if !strings.Contains(contentStr, "-----BEGIN CERTIFICATE-----") {
|
||||
t.Error("Expected PEM certificate headers")
|
||||
}
|
||||
if !strings.Contains(contentStr, "-----END CERTIFICATE-----") {
|
||||
t.Error("Expected PEM certificate footers")
|
||||
}
|
||||
|
||||
// Count certificates (should have 2)
|
||||
certCount := strings.Count(contentStr, "-----BEGIN CERTIFICATE-----")
|
||||
if certCount != 2 {
|
||||
t.Errorf("Expected 2 certificates, found %d", certCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRootsWithNegativeSerial(t *testing.T) {
|
||||
// Should call log.Fatalf due to invalid base64
|
||||
// Can't easily test this without subprocess, so we'll skip it
|
||||
t.Skip("Cannot easily test log.Fatalf without subprocess")
|
||||
}
|
||||
|
||||
func TestGenerateRootsHTTPError(t *testing.T) {
|
||||
// Should call log.Fatalf due to HTTP error
|
||||
// Can't easily test this without subprocess, so we'll skip it
|
||||
t.Skip("Cannot easily test log.Fatalf without subprocess")
|
||||
}
|
||||
|
||||
func TestGenerateRootsInvalidJSON(t *testing.T) {
|
||||
// Should call log.Fatalf due to JSON parse error
|
||||
// Can't easily test this without subprocess, so we'll skip it
|
||||
t.Skip("Cannot easily test log.Fatalf without subprocess")
|
||||
}
|
||||
|
||||
func TestGenerateRootsArgumentParsing(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
customOutputFile := filepath.Join(tmpDir, "custom-roots.pem")
|
||||
|
||||
mockResponse := CTLogRootsResponse{
|
||||
Certificates: []string{
|
||||
"MIIDFzCCAf+gAwIBAgIUbOZ5dIVBuGei8T0VdIKjCpACVgMwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw0yNTA4MjgxOTA4MjlaFw0yNjA4MjgxOTA4MjlaMBsxGTAXBgNVBAMMEFRlc3QgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEmF+JcVMD9u0efdi8AkIH4xiKb/8lJveOYu5UKJ/hE7j+QN0HcK+3d0Fh83Hgny7ZgF3A8JlTDnlZrkI1+n8uxLAbvZ/PVNS286FwlnWppnarSBwFMuHp+SoGFuNIQtB5LaxVt943Oxaw4vVaFBqi0iYPFKXj2JxLIwiwKxLiZ2hS15g/JIhRXSycHjMCvukPj1wtRo/cHF83Miydw+ngFqrfXcEzvG/Yx5qvAgowvG/FEKha5zB36ywy35SHq8gX7DHs77eQ52lxd4yvYfPhIwaAzTvnpyACcvfgP/XiETMxsaPoZ3WFzK4y3Smg6zUMLAB1YE4RxUd+4NA+NBmbAgMBAAGjUzBRMB0GA1UdDgQWBBQxgJBg/4o2f1e58aBMOlyEO0svIDAfBgNVHSMEGDAWgBQxgJBg/4o2f1e58aBMOlyEO0svIDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA1Z1r8pJeYizgo30kA+Q4SICq70jvAO6XPrin2hp8MlvE7nUG2afInzNh6FXjqMI0TWctqV+wF9nnCkPBPu4ybCbm7reiJ1SojPmp/qxv+qoyXXFZxX/ugOhhJv4BbSoHxepdSPBtqQb5uZPL/1q9vYmLfsEdEdh1dI4vEbrvvZR/fV/5+ZjL2uOuWf6zw0BpMqXyC0wcoZIWKHl62yLOhdJwbUVLfKiHuwrhsdIIc0QDI74U6x5pu/eIa1hCPX9e/X8vEK8/0EBV2xGhAeeteu4bY04AjrSs0tESQO5EfACmhQM/1ytlqMx3qsPGb7pIptUttHREvr+RL6qKCRZCK",
|
||||
},
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(mockResponse)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Test custom source and output
|
||||
args := []string{"--source", server.URL, "--output", customOutputFile}
|
||||
generateRoots(args, false, true, false)
|
||||
|
||||
// Verify custom output file was created
|
||||
if _, err := os.Stat(customOutputFile); os.IsNotExist(err) {
|
||||
t.Error("Custom output file was not created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRootsMissingSourceArgument(t *testing.T) {
|
||||
// Should call log.Fatal due to missing source URL argument
|
||||
// Can't easily test this without subprocess, so we'll skip it
|
||||
t.Skip("Cannot easily test log.Fatal without subprocess")
|
||||
}
|
||||
|
||||
func TestGenerateRootsMissingOutputArgument(t *testing.T) {
|
||||
// Should call log.Fatal due to missing output filename argument
|
||||
// Can't easily test this without subprocess, so we'll skip it
|
||||
t.Skip("Cannot easily test log.Fatal without subprocess")
|
||||
}
|
||||
|
||||
func TestGenerateRootsUnknownArgument(t *testing.T) {
|
||||
// Should call log.Fatalf due to unknown argument
|
||||
// Can't easily test this without subprocess, so we'll skip it
|
||||
t.Skip("Cannot easily test log.Fatalf without subprocess")
|
||||
}
|
||||
|
||||
func TestGenerateRootsDefaultArguments(t *testing.T) {
|
||||
// Test that default arguments are used correctly
|
||||
// This test would normally make a real HTTP request, so we'll skip it
|
||||
// unless we're doing integration tests
|
||||
t.Skip("Skipping test that would make real HTTP request")
|
||||
}
|
||||
|
||||
func TestGenerateRootsSourceURLFormatting(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
outputFile := filepath.Join(tmpDir, "test-roots.pem")
|
||||
|
||||
mockResponse := CTLogRootsResponse{
|
||||
Certificates: []string{
|
||||
"MIIDFzCCAf+gAwIBAgIUbOZ5dIVBuGei8T0VdIKjCpACVgMwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw0yNTA4MjgxOTA4MjlaFw0yNjA4MjgxOTA4MjlaMBsxGTAXBgNVBAMMEFRlc3QgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEmF+JcVMD9u0efdi8AkIH4xiKb/8lJveOYu5UKJ/hE7j+QN0HcK+3d0Fh83Hgny7ZgF3A8JlTDnlZrkI1+n8uxLAbvZ/PVNS286FwlnWppnarSBwFMuHp+SoGFuNIQtB5LaxVt943Oxaw4vVaFBqi0iYPFKXj2JxLIwiwKxLiZ2hS15g/JIhRXSycHjMCvukPj1wtRo/cHF83Miydw+ngFqrfXcEzvG/Yx5qvAgowvG/FEKha5zB36ywy35SHq8gX7DHs77eQ52lxd4yvYfPhIwaAzTvnpyACcvfgP/XiETMxsaPoZ3WFzK4y3Smg6zUMLAB1YE4RxUd+4NA+NBmbAgMBAAGjUzBRMB0GA1UdDgQWBBQxgJBg/4o2f1e58aBMOlyEO0svIDAfBgNVHSMEGDAWgBQxgJBg/4o2f1e58aBMOlyEO0svIDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA1Z1r8pJeYizgo30kA+Q4SICq70jvAO6XPrin2hp8MlvE7nUG2afInzNh6FXjqMI0TWctqV+wF9nnCkPBPu4ybCbm7reiJ1SojPmp/qxv+qoyXXFZxX/ugOhhJv4BbSoHxepdSPBtqQb5uZPL/1q9vYmLfsEdEdh1dI4vEbrvvZR/fV/5+ZjL2uOuWf6zw0BpMqXyC0wcoZIWKHl62yLOhdJwbUVLfKiHuwrhsdIIc0QDI74U6x5pu/eIa1hCPX9e/X8vEK8/0EBV2xGhAeeteu4bY04AjrSs0tESQO5EfACmhQM/1ytlqMx3qsPGb7pIptUttHREvr+RL6qKCRZCK",
|
||||
},
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify the path formatting
|
||||
expectedPath := "/ct/v1/get-roots"
|
||||
if r.URL.Path != expectedPath {
|
||||
t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(mockResponse)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Test with URL that doesn't end with /
|
||||
sourceURL := strings.TrimSuffix(server.URL, "/")
|
||||
args := []string{"--source", sourceURL, "--output", outputFile}
|
||||
generateRoots(args, false, true, false)
|
||||
|
||||
// Should still work (the function adds trailing slash)
|
||||
if _, err := os.Stat(outputFile); os.IsNotExist(err) {
|
||||
t.Error("Output file was not created when source URL lacks trailing slash")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPEMEncoding(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
outputFile := filepath.Join(tmpDir, "test-roots.pem")
|
||||
|
||||
mockResponse := CTLogRootsResponse{
|
||||
Certificates: []string{
|
||||
"MIIDFzCCAf+gAwIBAgIUbOZ5dIVBuGei8T0VdIKjCpACVgMwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw0yNTA4MjgxOTA4MjlaFw0yNjA4MjgxOTA4MjlaMBsxGTAXBgNVBAMMEFRlc3QgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEmF+JcVMD9u0efdi8AkIH4xiKb/8lJveOYu5UKJ/hE7j+QN0HcK+3d0Fh83Hgny7ZgF3A8JlTDnlZrkI1+n8uxLAbvZ/PVNS286FwlnWppnarSBwFMuHp+SoGFuNIQtB5LaxVt943Oxaw4vVaFBqi0iYPFKXj2JxLIwiwKxLiZ2hS15g/JIhRXSycHjMCvukPj1wtRo/cHF83Miydw+ngFqrfXcEzvG/Yx5qvAgowvG/FEKha5zB36ywy35SHq8gX7DHs77eQ52lxd4yvYfPhIwaAzTvnpyACcvfgP/XiETMxsaPoZ3WFzK4y3Smg6zUMLAB1YE4RxUd+4NA+NBmbAgMBAAGjUzBRMB0GA1UdDgQWBBQxgJBg/4o2f1e58aBMOlyEO0svIDAfBgNVHSMEGDAWgBQxgJBg/4o2f1e58aBMOlyEO0svIDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA1Z1r8pJeYizgo30kA+Q4SICq70jvAO6XPrin2hp8MlvE7nUG2afInzNh6FXjqMI0TWctqV+wF9nnCkPBPu4ybCbm7reiJ1SojPmp/qxv+qoyXXFZxX/ugOhhJv4BbSoHxepdSPBtqQb5uZPL/1q9vYmLfsEdEdh1dI4vEbrvvZR/fV/5+ZjL2uOuWf6zw0BpMqXyC0wcoZIWKHl62yLOhdJwbUVLfKiHuwrhsdIIc0QDI74U6x5pu/eIa1hCPX9e/X8vEK8/0EBV2xGhAeeteu4bY04AjrSs0tESQO5EfACmhQM/1ytlqMx3qsPGb7pIptUttHREvr+RL6qKCRZCK",
|
||||
},
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(mockResponse)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
args := []string{"--source", server.URL, "--output", outputFile}
|
||||
generateRoots(args, false, true, false)
|
||||
|
||||
// Read and verify PEM structure
|
||||
content, err := os.ReadFile(outputFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Parse PEM blocks
|
||||
var certCount int
|
||||
remaining := content
|
||||
for len(remaining) > 0 {
|
||||
block, rest := pem.Decode(remaining)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if block.Type != "CERTIFICATE" {
|
||||
t.Errorf("Expected block type 'CERTIFICATE', got %s", block.Type)
|
||||
}
|
||||
|
||||
certCount++
|
||||
remaining = rest
|
||||
}
|
||||
|
||||
if certCount != 1 {
|
||||
t.Errorf("Expected 1 certificate, found %d", certCount)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user