package utils import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "math/big" "net" "os" "path/filepath" "time" ) const ( certFileName = "cert.pem" keyFileName = "key.pem" ) // GenerateSelfSignedCert generates a self-signed TLS certificate func GenerateSelfSignedCert(certDir, host string) (certPath, keyPath string, err error) { // Create cert directory if it doesn't exist if err := os.MkdirAll(certDir, 0755); err != nil { return "", "", fmt.Errorf("failed to create cert directory: %w", err) } certPath = filepath.Join(certDir, certFileName) keyPath = filepath.Join(certDir, keyFileName) // Check if certificate already exists and is valid if certExists(certPath, keyPath) { if isCertValid(certPath) { return certPath, keyPath, nil } // Certificate exists but is invalid/expired, regenerate } // Generate private key using ECDSA P-256 privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return "", "", fmt.Errorf("failed to generate private key: %w", err) } // Generate serial number serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { return "", "", fmt.Errorf("failed to generate serial number: %w", err) } // Create certificate template notBefore := time.Now() notAfter := notBefore.Add(365 * 24 * time.Hour) // Valid for 1 year template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ Organization: []string{"WhatsHooked Self-Signed"}, CommonName: host, }, NotBefore: notBefore, NotAfter: notAfter, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } // Add host as SAN (Subject Alternative Name) if ip := net.ParseIP(host); ip != nil { template.IPAddresses = append(template.IPAddresses, ip) } else { template.DNSNames = append(template.DNSNames, host) } // Add localhost and common IPs as SANs template.DNSNames = append(template.DNSNames, "localhost") template.IPAddresses = append(template.IPAddresses, net.ParseIP("127.0.0.1"), net.ParseIP("::1"), ) // Create self-signed certificate derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) if err != nil { return "", "", fmt.Errorf("failed to create certificate: %w", err) } // Write certificate to file certFile, err := os.Create(certPath) if err != nil { return "", "", fmt.Errorf("failed to create cert file: %w", err) } defer certFile.Close() if err := pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { return "", "", fmt.Errorf("failed to write cert file: %w", err) } // Write private key to file keyFile, err := os.Create(keyPath) if err != nil { return "", "", fmt.Errorf("failed to create key file: %w", err) } defer keyFile.Close() privBytes, err := x509.MarshalECPrivateKey(privateKey) if err != nil { return "", "", fmt.Errorf("failed to marshal private key: %w", err) } if err := pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes}); err != nil { return "", "", fmt.Errorf("failed to write key file: %w", err) } return certPath, keyPath, nil } // certExists checks if both certificate and key files exist func certExists(certPath, keyPath string) bool { _, certErr := os.Stat(certPath) _, keyErr := os.Stat(keyPath) return certErr == nil && keyErr == nil } // isCertValid checks if a certificate file is valid and not expired func isCertValid(certPath string) bool { certPEM, err := os.ReadFile(certPath) if err != nil { return false } block, _ := pem.Decode(certPEM) if block == nil { return false } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return false } // Check if certificate is expired or will expire in the next 30 days now := time.Now() if now.Before(cert.NotBefore) || now.After(cert.NotAfter) { return false } // Regenerate if expiring within 30 days if cert.NotAfter.Sub(now) < 30*24*time.Hour { return false } return true } // ValidateCertificateFiles checks if custom certificate files exist and are valid func ValidateCertificateFiles(certPath, keyPath string) error { // Check if files exist if _, err := os.Stat(certPath); err != nil { return fmt.Errorf("certificate file not found: %w", err) } if _, err := os.Stat(keyPath); err != nil { return fmt.Errorf("key file not found: %w", err) } // Try to load the certificate to validate it certPEM, err := os.ReadFile(certPath) if err != nil { return fmt.Errorf("failed to read certificate: %w", err) } keyPEM, err := os.ReadFile(keyPath) if err != nil { return fmt.Errorf("failed to read key: %w", err) } // Decode certificate certBlock, _ := pem.Decode(certPEM) if certBlock == nil { return fmt.Errorf("failed to decode certificate PEM") } _, err = x509.ParseCertificate(certBlock.Bytes) if err != nil { return fmt.Errorf("failed to parse certificate: %w", err) } // Decode key keyBlock, _ := pem.Decode(keyPEM) if keyBlock == nil { return fmt.Errorf("failed to decode key PEM") } // Try parsing as different key types if _, err := x509.ParseECPrivateKey(keyBlock.Bytes); err != nil { if _, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes); err != nil { if _, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes); err != nil { return fmt.Errorf("failed to parse private key (tried EC, PKCS1, PKCS8): %w", err) } } } return nil }