Files
OwnCord/Server/auth/tls_test.go
T
J3vb d056229b28 fix: self-signed cert no longer generated as CA (BUG-138)
Changed IsCA to false, removed KeyUsageCertSign, and reduced validity
from 10 years to 2 years. A compromised key can no longer sign
additional certificates trusted by TOFU-pinning clients.
2026-04-02 13:03:02 +02:00

298 lines
8.1 KiB
Go

package auth_test
import (
"crypto/tls"
"crypto/x509"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/owncord/server/auth"
"github.com/owncord/server/config"
)
func TestGenerateSelfSignedCreatesFiles(t *testing.T) {
tmpDir := t.TempDir()
certFile := filepath.Join(tmpDir, "cert.pem")
keyFile := filepath.Join(tmpDir, "key.pem")
if err := auth.GenerateSelfSigned(certFile, keyFile); err != nil {
t.Fatalf("GenerateSelfSigned() error: %v", err)
}
if _, err := os.Stat(certFile); os.IsNotExist(err) {
t.Error("cert.pem not created")
}
if _, err := os.Stat(keyFile); os.IsNotExist(err) {
t.Error("key.pem not created")
}
}
func TestGenerateSelfSignedProducesValidCert(t *testing.T) {
tmpDir := t.TempDir()
certFile := filepath.Join(tmpDir, "cert.pem")
keyFile := filepath.Join(tmpDir, "key.pem")
if err := auth.GenerateSelfSigned(certFile, keyFile); err != nil {
t.Fatalf("GenerateSelfSigned() error: %v", err)
}
// Load the generated cert/key pair.
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
t.Fatalf("tls.LoadX509KeyPair error: %v", err)
}
// Parse the leaf certificate.
leaf, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
t.Fatalf("x509.ParseCertificate error: %v", err)
}
// Verify validity period is ~2 years (not the old 10y).
minExpiry := time.Now().Add(1 * 365 * 24 * time.Hour)
maxExpiry := time.Now().Add(3 * 365 * 24 * time.Hour)
if leaf.NotAfter.Before(minExpiry) {
t.Errorf("cert expires %v, expected at least 1 year from now (%v)", leaf.NotAfter, minExpiry)
}
if leaf.NotAfter.After(maxExpiry) {
t.Errorf("cert expires %v, expected at most 3 years from now (%v)", leaf.NotAfter, maxExpiry)
}
// BUG-138: Leaf cert must NOT be a CA — prevents signing other certs on key compromise.
if leaf.IsCA {
t.Error("expected IsCA = false for self-signed leaf cert")
}
if leaf.KeyUsage&x509.KeyUsageCertSign != 0 {
t.Error("leaf cert should not have KeyUsageCertSign")
}
}
func TestGenerateSelfSignedInvalidCertPath(t *testing.T) {
err := auth.GenerateSelfSigned("/nonexistent/dir/cert.pem", "/nonexistent/dir/key.pem")
if err == nil {
t.Error("GenerateSelfSigned() should error for invalid cert path")
}
}
func TestGenerateSelfSignedInvalidKeyPath(t *testing.T) {
tmpDir := t.TempDir()
certFile := filepath.Join(tmpDir, "cert.pem")
// Key path in non-existent dir.
err := auth.GenerateSelfSigned(certFile, "/nonexistent/dir/key.pem")
if err == nil {
t.Error("GenerateSelfSigned() should error for invalid key path")
}
}
func TestLoadOrGenerateSelfSigned(t *testing.T) {
tmpDir := t.TempDir()
certFile := filepath.Join(tmpDir, "cert.pem")
keyFile := filepath.Join(tmpDir, "key.pem")
cfg := config.TLSConfig{
Mode: "self_signed",
CertFile: certFile,
KeyFile: keyFile,
}
result, err := auth.LoadOrGenerate(cfg)
if err != nil {
t.Fatalf("LoadOrGenerate() error: %v", err)
}
if result.TLSConfig == nil {
t.Fatal("LoadOrGenerate() returned nil TLSConfig")
}
if len(result.TLSConfig.Certificates) == 0 {
t.Error("LoadOrGenerate() returned TLSConfig with no certificates")
}
if result.HTTPHandler != nil {
t.Error("self_signed mode should not set HTTPHandler")
}
}
func TestLoadOrGenerateLoadsExistingCert(t *testing.T) {
tmpDir := t.TempDir()
certFile := filepath.Join(tmpDir, "cert.pem")
keyFile := filepath.Join(tmpDir, "key.pem")
// Generate a cert first.
if err := auth.GenerateSelfSigned(certFile, keyFile); err != nil {
t.Fatalf("GenerateSelfSigned() error: %v", err)
}
cfg := config.TLSConfig{
Mode: "self_signed",
CertFile: certFile,
KeyFile: keyFile,
}
// Load the existing cert (should not regenerate).
result, err := auth.LoadOrGenerate(cfg)
if err != nil {
t.Fatalf("LoadOrGenerate() error: %v", err)
}
if len(result.TLSConfig.Certificates) == 0 {
t.Error("LoadOrGenerate() returned no certificates")
}
}
func TestLoadOrGenerateModeOff(t *testing.T) {
cfg := config.TLSConfig{Mode: "off"}
result, err := auth.LoadOrGenerate(cfg)
if err != nil {
t.Fatalf("LoadOrGenerate(mode=off) error: %v", err)
}
if result.TLSConfig != nil {
t.Error("LoadOrGenerate(mode=off) should return nil TLSConfig")
}
}
func TestLoadOrGenerateModeManualMissingFiles(t *testing.T) {
cfg := config.TLSConfig{
Mode: "manual",
CertFile: "/nonexistent/cert.pem",
KeyFile: "/nonexistent/key.pem",
}
_, err := auth.LoadOrGenerate(cfg)
if err == nil {
t.Error("LoadOrGenerate(mode=manual) should error when cert/key don't exist")
}
}
func TestLoadOrGenerateModeManualValidFiles(t *testing.T) {
tmpDir := t.TempDir()
certFile := filepath.Join(tmpDir, "cert.pem")
keyFile := filepath.Join(tmpDir, "key.pem")
// Pre-generate cert files.
if err := auth.GenerateSelfSigned(certFile, keyFile); err != nil {
t.Fatalf("GenerateSelfSigned() error: %v", err)
}
cfg := config.TLSConfig{
Mode: "manual",
CertFile: certFile,
KeyFile: keyFile,
}
result, err := auth.LoadOrGenerate(cfg)
if err != nil {
t.Fatalf("LoadOrGenerate(mode=manual) error: %v", err)
}
if len(result.TLSConfig.Certificates) == 0 {
t.Error("LoadOrGenerate(mode=manual) returned no certificates")
}
}
func TestLoadOrGenerateUnknownMode(t *testing.T) {
cfg := config.TLSConfig{Mode: "unknown_mode"}
_, err := auth.LoadOrGenerate(cfg)
if err == nil {
t.Error("LoadOrGenerate() should error for unknown TLS mode")
}
}
// ── ACME mode tests ───────────────────────────────────────────────────────
func TestLoadOrGenerateACME_MissingDomain(t *testing.T) {
cfg := config.TLSConfig{Mode: "acme", Domain: ""}
_, err := auth.LoadOrGenerate(cfg)
if err == nil {
t.Fatal("expected error for ACME mode without domain")
}
if !strings.Contains(err.Error(), "domain") {
t.Errorf("error should mention domain, got: %v", err)
}
}
func TestLoadOrGenerateACME_IPAddress(t *testing.T) {
cfg := config.TLSConfig{Mode: "acme", Domain: "192.168.1.1"}
_, err := auth.LoadOrGenerate(cfg)
if err == nil {
t.Fatal("expected error for ACME mode with IP address")
}
if !strings.Contains(err.Error(), "IP address") {
t.Errorf("error should mention IP address, got: %v", err)
}
}
func TestLoadOrGenerateACME_WildcardDomain(t *testing.T) {
cfg := config.TLSConfig{Mode: "acme", Domain: "*.example.com"}
_, err := auth.LoadOrGenerate(cfg)
if err == nil {
t.Fatal("expected error for ACME mode with wildcard domain")
}
if !strings.Contains(err.Error(), "wildcard") {
t.Errorf("error should mention wildcard, got: %v", err)
}
}
func TestLoadOrGenerateACME_ValidDomain(t *testing.T) {
tmpDir := t.TempDir()
cacheDir := filepath.Join(tmpDir, "acme_certs")
cfg := config.TLSConfig{
Mode: "acme",
Domain: "chat.example.com",
AcmeCacheDir: cacheDir,
}
result, err := auth.LoadOrGenerate(cfg)
if err != nil {
t.Fatalf("LoadOrGenerate(acme) error: %v", err)
}
if result.TLSConfig == nil {
t.Fatal("ACME mode should return non-nil TLSConfig")
}
if result.TLSConfig.GetCertificate == nil {
t.Error("ACME TLSConfig should have GetCertificate set")
}
if result.HTTPHandler == nil {
t.Error("ACME mode should return non-nil HTTPHandler")
}
// Verify cache directory was created.
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
t.Error("ACME cache directory was not created")
}
}
func TestLoadOrGenerateACME_HTTPRedirect(t *testing.T) {
tmpDir := t.TempDir()
cfg := config.TLSConfig{
Mode: "acme",
Domain: "chat.example.com",
AcmeCacheDir: filepath.Join(tmpDir, "acme_certs"),
}
result, err := auth.LoadOrGenerate(cfg)
if err != nil {
t.Fatalf("LoadOrGenerate(acme) error: %v", err)
}
// Non-challenge requests should redirect to HTTPS.
req := httptest.NewRequest(http.MethodGet, "http://chat.example.com/some/path", nil)
rec := httptest.NewRecorder()
result.HTTPHandler.ServeHTTP(rec, req)
if rec.Code != http.StatusMovedPermanently {
t.Errorf("expected 301 redirect, got %d", rec.Code)
}
loc := rec.Header().Get("Location")
if !strings.HasPrefix(loc, "https://chat.example.com/") {
t.Errorf("redirect should point to HTTPS, got: %s", loc)
}
}