// Package certmanager provides tools for managing Root CAs and generating SSL certificates. package certmanager import ( "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "math/big" "net" "os" "path/filepath" "time" ) // CertificateManager handles CA and certificate generation. type CertificateManager struct { CertsDir string CommonName string // CN for generated server certs; defaults to "localhost" if empty } // NewCertificateManager creates a new CertificateManager. func NewCertificateManager(certsDir string) *CertificateManager { return &CertificateManager{CertsDir: certsDir} } // GetCACertPath returns the path to the CA certificate. func (cm *CertificateManager) GetCACertPath() string { return filepath.Join(cm.CertsDir, "ca.crt") } // GetCAKeyPath returns the path to the CA private key. func (cm *CertificateManager) GetCAKeyPath() string { return filepath.Join(cm.CertsDir, "ca.key") } // EnsureCA ensures that a CA certificate and key exist. func (cm *CertificateManager) EnsureCA() error { certPath := cm.GetCACertPath() keyPath := cm.GetCAKeyPath() if _, err := os.Stat(certPath); err == nil { if _, err := os.Stat(keyPath); err == nil { return nil } } return cm.GenerateCA() } // GetServerCertPEMPath returns the path to the server certificate PEM. func (cm *CertificateManager) GetServerCertPEMPath() string { return filepath.Join(cm.CertsDir, "server.crt") } // GetServerKeyPEMPath returns the path to the server private key PEM. func (cm *CertificateManager) GetServerKeyPEMPath() string { return filepath.Join(cm.CertsDir, "server.key") } // GetServerTLSConfig returns a TLS config with the server certificate. // If the certificate doesn't exist, it generates one for the given domains. func (cm *CertificateManager) GetServerTLSConfig(domains []string) (*tls.Config, error) { certPath := cm.GetServerCertPEMPath() keyPath := cm.GetServerKeyPEMPath() generate := false if _, err := os.Stat(certPath); os.IsNotExist(err) { generate = true } else { // Check if the current certificate covers all requested domains certBytes, err := os.ReadFile(certPath) if err == nil { block, _ := pem.Decode(certBytes) if block != nil { cert, err := x509.ParseCertificate(block.Bytes) if err == nil { domainMap := make(map[string]bool) for _, d := range cert.DNSNames { domainMap[d] = true } for _, ip := range cert.IPAddresses { domainMap[ip.String()] = true } for _, d := range domains { if !domainMap[d] { generate = true break } } } else { generate = true } } else { generate = true } } else { generate = true } } if generate { certPEM, keyPEM, err := cm.GenerateCertificate(domains) if err != nil { return nil, err } if err := os.WriteFile(certPath, certPEM, 0644); err != nil { return nil, err } if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil { return nil, err } } cert, err := tls.LoadX509KeyPair(certPath, keyPath) if err != nil { return nil, err } return &tls.Config{ Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12, CipherSuites: []uint16{ tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, tls.TLS_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_RSA_WITH_AES_256_GCM_SHA384, }, }, nil } // GenerateCA generates a new CA certificate and key. func (cm *CertificateManager) GenerateCA() error { priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return err } notBefore := time.Now() notAfter := notBefore.Add(10 * 365 * 24 * time.Hour) // 10 years serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { return err } template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ Organization: []string{"AfterTouch"}, CommonName: "AfterTouch Local Root CA", }, NotBefore: notBefore, NotAfter: notAfter, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, IsCA: true, } derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) if err != nil { return err } certPath := cm.GetCACertPath() if mkdirErr := os.MkdirAll(cm.CertsDir, 0755); mkdirErr != nil { return mkdirErr } certOut, err := os.Create(certPath) if err != nil { return err } certEncodeErr := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) certCloseErr := certOut.Close() if certEncodeErr != nil { return certEncodeErr } if certCloseErr != nil { return fmt.Errorf("close certificate file: %w", certCloseErr) } keyOut, err := os.OpenFile(cm.GetCAKeyPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } keyEncodeErr := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) keyCloseErr := keyOut.Close() if keyEncodeErr != nil { return keyEncodeErr } if keyCloseErr != nil { return fmt.Errorf("close key file: %w", keyCloseErr) } return nil } // GenerateCertificate generates a certificate for the given domains signed by the CA. func (cm *CertificateManager) GenerateCertificate(domains []string) ([]byte, []byte, error) { if err := cm.EnsureCA(); err != nil { return nil, nil, err } caCertPEM, err := os.ReadFile(cm.GetCACertPath()) if err != nil { return nil, nil, err } caKeyPEM, err := os.ReadFile(cm.GetCAKeyPath()) if err != nil { return nil, nil, err } caBlock, _ := pem.Decode(caCertPEM) caCert, err := x509.ParseCertificate(caBlock.Bytes) if err != nil { return nil, nil, err } keyBlock, _ := pem.Decode(caKeyPEM) caKey, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes) if err != nil { return nil, nil, err } priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, nil, err } notBefore := time.Now() notAfter := notBefore.Add(365 * 24 * time.Hour) // 1 year serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { return nil, nil, err } var ( dnsNames []string ipAddresses []net.IP ) for _, domain := range domains { if ip := net.ParseIP(domain); ip != nil { ipAddresses = append(ipAddresses, ip) } else { dnsNames = append(dnsNames, domain) } } cn := cm.CommonName if cn == "" { cn = "localhost" } template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ Organization: []string{"AfterTouch"}, CommonName: cn, }, NotBefore: notBefore, NotAfter: notAfter, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, DNSNames: dnsNames, IPAddresses: ipAddresses, } derBytes, err := x509.CreateCertificate(rand.Reader, &template, caCert, &priv.PublicKey, caKey) if err != nil { return nil, nil, err } certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) return certPEM, keyPEM, nil }