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, "") { 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") }