238 lines
4.9 KiB
Go
238 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
cryptoRand "crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/big"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
localTLSOnce sync.Once
|
|
localTLSCertPath string
|
|
localTLSKeyPath string
|
|
localTLSErr error
|
|
)
|
|
|
|
func localTLSPathsCached() (string, string, error) {
|
|
localTLSOnce.Do(func() {
|
|
localTLSCertPath, localTLSKeyPath, localTLSErr = ensureLocalTLSCertificate()
|
|
})
|
|
|
|
return localTLSCertPath, localTLSKeyPath, localTLSErr
|
|
}
|
|
|
|
func localTLSDir() (string, error) {
|
|
if dir, err := os.UserConfigDir(); err == nil && strings.TrimSpace(dir) != "" {
|
|
return filepath.Join(dir, "nsfwapp", "tls"), nil
|
|
}
|
|
|
|
home, err := os.UserHomeDir()
|
|
if err != nil || strings.TrimSpace(home) == "" {
|
|
return "", fmt.Errorf("user config dir konnte nicht ermittelt werden")
|
|
}
|
|
|
|
return filepath.Join(home, ".config", "nsfwapp", "tls"), nil
|
|
}
|
|
|
|
func localTLSHosts() []string {
|
|
seen := map[string]bool{}
|
|
out := []string{}
|
|
|
|
add := func(host string) {
|
|
host = normalizeOriginHost(host)
|
|
if host == "" || seen[host] {
|
|
return
|
|
}
|
|
seen[host] = true
|
|
out = append(out, host)
|
|
}
|
|
|
|
for _, origin := range configuredPublicAppOrigins() {
|
|
u, err := url.Parse(strings.TrimSpace(origin))
|
|
if err == nil {
|
|
add(u.Hostname())
|
|
}
|
|
}
|
|
|
|
for _, host := range localAppHostCandidates() {
|
|
add(host)
|
|
}
|
|
|
|
add("localhost")
|
|
add("127.0.0.1")
|
|
add("::1")
|
|
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func tlsCertificateCoversHosts(certPath string, hosts []string) bool {
|
|
cert, err := readLeafCertificate(certPath)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
now := time.Now()
|
|
if now.Before(cert.NotBefore) || now.After(cert.NotAfter.Add(-30*24*time.Hour)) {
|
|
return false
|
|
}
|
|
|
|
for _, host := range hosts {
|
|
host = normalizeOriginHost(host)
|
|
if host == "" {
|
|
continue
|
|
}
|
|
if err := cert.VerifyHostname(host); err != nil {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func readLeafCertificate(certPath string) (*x509.Certificate, error) {
|
|
b, err := os.ReadFile(certPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
block, _ := pem.Decode(b)
|
|
if block == nil || block.Type != "CERTIFICATE" {
|
|
return nil, fmt.Errorf("keine PEM-Zertifikatsdaten")
|
|
}
|
|
|
|
return x509.ParseCertificate(block.Bytes)
|
|
}
|
|
|
|
func ensureLocalTLSCertificate() (string, string, error) {
|
|
dir, err := localTLSDir()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
certPath := filepath.Join(dir, "recorder-cert.pem")
|
|
keyPath := filepath.Join(dir, "recorder-key.pem")
|
|
hosts := localTLSHosts()
|
|
|
|
if fileExistsNonEmpty(certPath) &&
|
|
fileExistsNonEmpty(keyPath) &&
|
|
tlsCertificateCoversHosts(certPath, hosts) {
|
|
appLogln("🔐 TLS-Zertifikat aktiv:", certPath)
|
|
return certPath, keyPath, nil
|
|
}
|
|
|
|
if err := generateLocalTLSCertificate(certPath, keyPath, hosts); err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
appLogln("🔐 TLS-Zertifikat neu erstellt:", certPath)
|
|
appLogln("🔐 TLS-Namen:", strings.Join(hosts, ", "))
|
|
return certPath, keyPath, nil
|
|
}
|
|
|
|
func generateLocalTLSCertificate(certPath string, keyPath string, hosts []string) error {
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), cryptoRand.Reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
serialLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
|
serial, err := cryptoRand.Int(cryptoRand.Reader, serialLimit)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
notBefore := time.Now().Add(-1 * time.Hour)
|
|
notAfter := notBefore.Add(397 * 24 * time.Hour)
|
|
|
|
tpl := &x509.Certificate{
|
|
SerialNumber: serial,
|
|
Subject: pkix.Name{
|
|
CommonName: "nsfwapp local",
|
|
Organization: []string{"nsfwapp"},
|
|
},
|
|
NotBefore: notBefore,
|
|
NotAfter: notAfter,
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
BasicConstraintsValid: true,
|
|
}
|
|
|
|
seenDNS := map[string]bool{}
|
|
seenIP := map[string]bool{}
|
|
for _, host := range hosts {
|
|
host = normalizeOriginHost(host)
|
|
if host == "" {
|
|
continue
|
|
}
|
|
|
|
if ip := net.ParseIP(host); ip != nil {
|
|
key := ip.String()
|
|
if !seenIP[key] {
|
|
seenIP[key] = true
|
|
tpl.IPAddresses = append(tpl.IPAddresses, ip)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if !seenDNS[host] {
|
|
seenDNS[host] = true
|
|
tpl.DNSNames = append(tpl.DNSNames, host)
|
|
}
|
|
}
|
|
|
|
if len(tpl.DNSNames) == 0 && len(tpl.IPAddresses) == 0 {
|
|
tpl.DNSNames = []string{"localhost"}
|
|
}
|
|
|
|
certDER, err := x509.CreateCertificate(
|
|
cryptoRand.Reader,
|
|
tpl,
|
|
tpl,
|
|
&key.PublicKey,
|
|
key,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keyDER, err := x509.MarshalECPrivateKey(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
certPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: certDER,
|
|
})
|
|
keyPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "EC PRIVATE KEY",
|
|
Bytes: keyDER,
|
|
})
|
|
|
|
if err := os.WriteFile(certPath, certPEM, 0o600); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|