violet/certs/certs.go

222 lines
4.8 KiB
Go
Raw Normal View History

2023-04-22 18:11:21 +01:00
package certs
import (
"crypto/tls"
"crypto/x509/pkix"
2023-04-22 18:11:21 +01:00
"fmt"
2023-07-22 01:11:47 +01:00
"github.com/1f349/violet/utils"
2024-04-20 16:17:32 +01:00
"github.com/mrmelon54/certgen"
"github.com/mrmelon54/rescheduler"
2023-04-22 18:11:21 +01:00
"io/fs"
"log"
"math/big"
"os"
"strings"
2023-04-22 18:11:21 +01:00
"sync"
"sync/atomic"
"time"
2023-04-22 18:11:21 +01:00
)
2023-04-24 01:35:23 +01:00
// Certs is the certificate loader and management system.
2023-04-22 18:11:21 +01:00
type Certs struct {
cDir fs.FS
kDir fs.FS
ss bool
2023-04-22 18:11:21 +01:00
s *sync.RWMutex
m map[string]*tls.Certificate
ca *certgen.CertGen
sn atomic.Int64
r *rescheduler.Rescheduler
t *time.Ticker
ts chan struct{}
2023-04-22 18:11:21 +01:00
}
2023-04-24 01:35:23 +01:00
// New creates a new cert list
func New(certDir fs.FS, keyDir fs.FS, selfCert bool) *Certs {
c := &Certs{
2023-04-22 18:11:21 +01:00
cDir: certDir,
kDir: keyDir,
ss: selfCert,
2023-04-22 18:11:21 +01:00
s: &sync.RWMutex{},
m: make(map[string]*tls.Certificate),
ts: make(chan struct{}, 1),
2023-04-22 18:11:21 +01:00
}
if !selfCert {
// the rescheduler isn't even used in self cert mode so why initialise it
c.r = rescheduler.NewRescheduler(c.threadCompile)
c.t = time.NewTicker(2 * time.Hour)
go func() {
for {
select {
case <-c.t.C:
c.Compile()
case <-c.ts:
return
}
}
}()
} else {
// in self-signed mode generate a CA certificate to sign other certificates
ca, err := certgen.MakeCaTls(4096, pkix.Name{
Country: []string{"GB"},
Organization: []string{"Violet"},
OrganizationalUnit: []string{"Development"},
SerialNumber: "0",
CommonName: fmt.Sprintf("%d.violet.test", time.Now().Unix()),
}, big.NewInt(0), func(now time.Time) time.Time {
return now.AddDate(10, 0, 0)
})
if err != nil {
log.Fatalln("Failed to generate CA cert for self-signed mode:", err)
}
c.ca = ca
}
return c
2023-04-22 18:11:21 +01:00
}
func (c *Certs) GetCertForDomain(domain string) *tls.Certificate {
// safety read lock
c.s.RLock()
defer c.s.RUnlock()
// lookup and return cert
if cert, ok := c.m[domain]; ok {
return cert
}
// if self-signed certificate is enabled then generate a certificate
if c.ss {
sn := c.sn.Add(1)
serverTls, err := certgen.MakeServerTls(c.ca, 4096, pkix.Name{
Country: []string{"GB"},
Organization: []string{domain},
OrganizationalUnit: []string{domain},
SerialNumber: fmt.Sprintf("%d", sn),
CommonName: domain,
}, big.NewInt(sn), func(now time.Time) time.Time {
return now.AddDate(10, 0, 0)
}, []string{domain}, nil)
if err != nil {
return nil
}
// save the generated leaf for loading if the domain is requested again
leaf := serverTls.GetTlsLeaf()
c.m[domain] = &leaf
return &leaf
}
2023-04-22 18:11:21 +01:00
// lookup and return wildcard cert
if wildcardDomain, ok := utils.ReplaceSubdomainWithWildcard(domain); ok {
if cert, ok := c.m[wildcardDomain]; ok {
return cert
}
}
// no cert found
return nil
}
// Compile loads the certificates and keys from the directories.
//
// This method makes use of the rescheduler instead of just ignoring multiple
// calls.
2023-04-22 18:11:21 +01:00
func (c *Certs) Compile() {
// don't bother compiling in self-signed mode
if c.ss {
return
}
c.r.Run()
}
func (c *Certs) Stop() {
if c.t != nil {
c.t.Stop()
}
close(c.ts)
}
func (c *Certs) threadCompile() {
// new map
certMap := make(map[string]*tls.Certificate)
2023-04-22 22:18:39 +01:00
// compile map and check errors
err := c.internalCompile(certMap)
if err != nil {
log.Printf("[Certs] Compile failed: %s\n", err)
return
}
2023-04-24 01:35:23 +01:00
// lock while replacing the map
c.s.Lock()
c.m = certMap
c.s.Unlock()
2023-04-22 18:11:21 +01:00
}
2023-04-24 01:35:23 +01:00
// internalCompile is a hidden internal method for loading the certificate and
// key files
2023-04-22 18:11:21 +01:00
func (c *Certs) internalCompile(m map[string]*tls.Certificate) error {
2023-06-04 22:28:48 +01:00
if c.cDir == nil {
return nil
}
2023-04-22 18:11:21 +01:00
// try to read dir
2023-06-05 22:23:28 +01:00
files, err := fs.ReadDir(c.cDir, ".")
2023-04-22 18:11:21 +01:00
if err != nil {
return fmt.Errorf("failed to read cert dir: %w", err)
}
log.Printf("[Certs] Compiling lookup table for %d certificates\n", len(files))
// find and parse certs
for _, i := range files {
// skip dirs
if i.IsDir() {
continue
}
// get file name and extension
name := i.Name()
if !strings.HasSuffix(name, ".cert.pem") {
continue
}
keyName := name[:len(name)-len("cert.pem")] + "key.pem"
2023-04-22 18:11:21 +01:00
// try to read cert file
certData, err := fs.ReadFile(c.cDir, name)
if err != nil {
return fmt.Errorf("failed to read cert file '%s': %w", name, err)
}
// try to read key file
keyData, err := fs.ReadFile(c.kDir, keyName)
if err != nil {
// ignore the file if the certificate doesn't exist
if os.IsNotExist(err) {
continue
}
2023-04-22 18:11:21 +01:00
return fmt.Errorf("failed to read key file '%s': %w", keyName, err)
}
// load key pair
pair, err := tls.X509KeyPair(certData, keyData)
if err != nil {
return fmt.Errorf("failed to load x509 key pair '%s + %s': %w", name, keyName, err)
}
// load tls leaf
cert := &pair
leaf := certgen.TlsLeaf(cert)
// save in map under each dns name
for _, j := range leaf.DNSNames {
m[j] = cert
}
}
// well no errors happened
return nil
}