Skip to content
Snippets Groups Projects
manager.go 8 KiB
Newer Older
  • Learn to ignore specific revisions
  • ale's avatar
    ale committed
    package acmeserver
    
    import (
    	"context"
    	"crypto"
    	"crypto/ecdsa"
    	"crypto/elliptic"
    	"crypto/rand"
    	"crypto/rsa"
    	"crypto/x509"
    	"crypto/x509/pkix"
    	"encoding/pem"
    	"errors"
    	"io"
    	"log"
    
    ale's avatar
    ale committed
    	"sync"
    
    ale's avatar
    ale committed
    	"time"
    
    
    	"git.autistici.org/ai3/replds"
    
    ale's avatar
    ale committed
    	"github.com/prometheus/client_golang/prometheus"
    
    ale's avatar
    ale committed
    )
    
    
    ale's avatar
    ale committed
    // certInfo represents what we know about the state of the certificate
    // at runtime.
    
    ale's avatar
    ale committed
    type certInfo struct {
    
    ale's avatar
    ale committed
    	config        *certConfig
    
    ale's avatar
    ale committed
    	retryDeadline time.Time
    
    ale's avatar
    ale committed
    	// Write-only attributes (not part of the renewal logic) to
    	// indicate whether we think we have a valid certificate or
    	// not. Used for monitoring and debugging.
    	valid     bool
    	expiresAt time.Time
    }
    
    func (i *certInfo) cn() string {
    	return i.config.Names[0]
    }
    
    func (i *certInfo) setOk(cert *x509.Certificate, renewalDays int) {
    	i.retryDeadline = cert.NotAfter.AddDate(0, 0, -renewalDays)
    	i.expiresAt = cert.NotAfter
    	i.valid = true
    
    	l := prometheus.Labels{"cn": i.cn()}
    	certOk.With(l).Set(1)
    }
    
    func (i *certInfo) setErr() {
    	now := time.Now()
    	i.retryDeadline = now.Add(errorRetryTimeout)
    	i.valid = false
    	if !i.expiresAt.IsZero() {
    		i.valid = i.expiresAt.After(now)
    	}
    
    	l := prometheus.Labels{"cn": i.cn()}
    	certRenewalErrorCount.With(l).Inc()
    	certOk.With(l).Set(b2f(i.valid))
    
    ale's avatar
    ale committed
    }
    
    type certStorage interface {
    	GetCert(string) ([][]byte, crypto.Signer, error)
    	PutCert(string, [][]byte, crypto.Signer) error
    }
    
    
    ale's avatar
    ale committed
    // CertGenerator is an interface to something that can generate a new
    // certificate, given a list of domains and a private key. The ACME
    // type implements this interface.
    
    ale's avatar
    ale committed
    type CertGenerator interface {
    
    ale's avatar
    ale committed
    	GetCertificate(context.Context, crypto.Signer, *certConfig) ([][]byte, *x509.Certificate, error)
    
    ale's avatar
    ale committed
    }
    
    // Manager periodically renews certificates before they expire, and
    // responds to http-01 validation requests.
    type Manager struct {
    
    ale's avatar
    ale committed
    	configDirs  []string
    
    ale's avatar
    ale committed
    	useRSA      bool
    	storage     certStorage
    	certs       []*certInfo
    	certGen     CertGenerator
    	renewalDays int
    
    	configCh chan []*certConfig
    
    ale's avatar
    ale committed
    	doneCh   chan bool
    }
    
    // NewManager creates a new Manager with the given configuration.
    func NewManager(config *Config, certGen CertGenerator) (*Manager, error) {
    	// Validate the configuration.
    
    ale's avatar
    ale committed
    	if len(config.Dirs) == 0 {
    		return nil, errors.New("configuration parameter 'config_dirs' is unset")
    	}
    	if config.Output.Path == "" {
    		return nil, errors.New("'output.path' is unset")
    
    ale's avatar
    ale committed
    	}
    
    	m := &Manager{
    
    ale's avatar
    ale committed
    		useRSA:      config.UseRSA,
    
    ale's avatar
    ale committed
    		configDirs:  config.Dirs,
    
    ale's avatar
    ale committed
    		doneCh:      make(chan bool),
    		configCh:    make(chan []*certConfig, 1),
    		certGen:     certGen,
    		renewalDays: config.RenewalDays,
    	}
    	if m.renewalDays <= 0 {
    		m.renewalDays = 15
    
    ale's avatar
    ale committed
    	}
    
    
    ale's avatar
    ale committed
    	ds := &dirStorage{root: config.Output.Path}
    	if config.Output.ReplDS == nil {
    
    ale's avatar
    ale committed
    		m.storage = ds
    	} else {
    
    ale's avatar
    ale committed
    		r, err := replds.NewPublicClient(config.Output.ReplDS)
    
    ale's avatar
    ale committed
    		if err != nil {
    			return nil, err
    		}
    		m.storage = &replStorage{
    			dirStorage: ds,
    
    ale's avatar
    ale committed
    		}
    	}
    
    	return m, nil
    }
    
    // Start the renewal processes. Canceling the provided context will
    
    ale's avatar
    ale committed
    // cause background processing to stop, interrupting all running
    // updates.
    
    ale's avatar
    ale committed
    func (m *Manager) Start(ctx context.Context) error {
    
    ale's avatar
    ale committed
    	domains, err := readCertConfigsFromDirs(m.configDirs)
    
    ale's avatar
    ale committed
    	if err != nil {
    		return err
    	}
    	m.configCh <- domains
    	go func() {
    		m.loop(ctx)
    		close(m.doneCh)
    	}()
    	return nil
    }
    
    
    ale's avatar
    ale committed
    // Wait for the Manager to terminate once it has been canceled.
    func (m *Manager) Wait() {
    
    ale's avatar
    ale committed
    	<-m.doneCh
    }
    
    // Reload configuration.
    func (m *Manager) Reload() {
    
    ale's avatar
    ale committed
    	domains, err := readCertConfigsFromDirs(m.configDirs)
    
    ale's avatar
    ale committed
    	if err != nil {
    		log.Printf("error reading config: %v", err)
    		return
    	}
    	m.configCh <- domains
    	log.Printf("configuration reloaded")
    }
    
    var (
    	renewalTimeout    = 10 * time.Minute
    	errorRetryTimeout = 10 * time.Minute
    )
    
    
    ale's avatar
    ale committed
    func (m *Manager) updateAllCerts(ctx context.Context, certs []*certInfo) {
    	for _, certInfo := range certs {
    
    		if certInfo.retryDeadline.After(time.Now()) {
    
    ale's avatar
    ale committed
    			continue
    		}
    
    ale's avatar
    ale committed
    
    		// Abort the loop if our context is canceled.
    		select {
    		case <-ctx.Done():
    			return
    		default:
    		}
    
    
    ale's avatar
    ale committed
    		certRenewalCount.With(prometheus.Labels{"cn": certInfo.cn()}).Inc()
    
    ale's avatar
    ale committed
    		uctx, cancel := context.WithTimeout(ctx, renewalTimeout)
    
    ale's avatar
    ale committed
    		leaf, err := m.updateCert(uctx, certInfo)
    
    ale's avatar
    ale committed
    		cancel()
    
    ale's avatar
    ale committed
    		if err == nil {
    			log.Printf("successfully renewed %s", certInfo.cn())
    			certInfo.setOk(leaf, m.renewalDays)
    		} else {
    			log.Printf("error updating %s: %v", certInfo.cn(), err)
    			certInfo.setErr()
    
    ale's avatar
    ale committed
    		}
    	}
    }
    
    
    ale's avatar
    ale committed
    func (m *Manager) updateCert(ctx context.Context, certInfo *certInfo) (*x509.Certificate, error) {
    
    ale's avatar
    ale committed
    	// Create a new private key.
    	var (
    		err error
    		key crypto.Signer
    	)
    	if m.useRSA {
    		key, err = rsa.GenerateKey(rand.Reader, 2048)
    	} else {
    		key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    	}
    	if err != nil {
    
    ale's avatar
    ale committed
    		return nil, err
    
    ale's avatar
    ale committed
    	}
    
    
    ale's avatar
    ale committed
    	der, leaf, err := m.certGen.GetCertificate(ctx, key, certInfo.config)
    
    ale's avatar
    ale committed
    	if err != nil {
    
    ale's avatar
    ale committed
    		return nil, err
    
    ale's avatar
    ale committed
    	}
    
    
    ale's avatar
    ale committed
    	if err := m.storage.PutCert(certInfo.cn(), der, key); err != nil {
    		return nil, err
    
    ale's avatar
    ale committed
    	}
    
    
    ale's avatar
    ale committed
    	return leaf, nil
    
    ale's avatar
    ale committed
    }
    
    // Replace the current configuration.
    
    ale's avatar
    ale committed
    func (m *Manager) loadConfig(certs []*certConfig) []*certInfo {
    	var out []*certInfo
    	for _, c := range certs {
    		cn := c.Names[0]
    
    ale's avatar
    ale committed
    		certInfo := &certInfo{
    
    ale's avatar
    ale committed
    			config: c,
    
    ale's avatar
    ale committed
    		}
    		pub, priv, err := m.storage.GetCert(cn)
    		if err != nil {
    			log.Printf("cert for %s is missing", cn)
    		} else {
    			// By calling validCert we catch things like subjectAltName changes.
    
    ale's avatar
    ale committed
    			leaf, err := validCert(c.Names, pub, priv)
    
    ale's avatar
    ale committed
    			if err == nil {
    				log.Printf("cert for %s loaded from storage", cn)
    
    ale's avatar
    ale committed
    				certInfo.setOk(leaf, m.renewalDays)
    
    ale's avatar
    ale committed
    			} else {
    				log.Printf("cert for %s loaded from storage but parameters have changed", cn)
    			}
    		}
    
    ale's avatar
    ale committed
    		out = append(out, certInfo)
    
    ale's avatar
    ale committed
    	}
    
    ale's avatar
    ale committed
    	return out
    
    ale's avatar
    ale committed
    }
    
    
    // This channel is used by the testing code to trigger an update,
    // without having to wait for the timer to tick.
    var testUpdateCh = make(chan bool)
    
    
    ale's avatar
    ale committed
    func (m *Manager) loop(ctx context.Context) {
    
    ale's avatar
    ale committed
    	// Updates are long-term jobs, so they should be
    	// interruptible. We run updates in a separate goroutine, and
    	// cancel them when the configuration is reloaded or on exit.
    	var upCancel context.CancelFunc
    	var wg sync.WaitGroup
    
    	startUpdate := func(certs []*certInfo) context.CancelFunc {
    		// Ensure the previous update has finished.
    		wg.Wait()
    
    		upCtx, cancel := context.WithCancel(ctx)
    		wg.Add(1)
    		go func() {
    			m.updateAllCerts(upCtx, certs)
    			wg.Done()
    		}()
    		return cancel
    	}
    
    
    ale's avatar
    ale committed
    	// Cancel the running update, if any. Called on config
    	// updates, when exiting.
    
    ale's avatar
    ale committed
    	cancelUpdate := func() {
    		if upCancel != nil {
    			upCancel()
    		}
    		wg.Wait()
    	}
    	defer cancelUpdate()
    
    
    ale's avatar
    ale committed
    	tick := time.NewTicker(5 * time.Minute)
    
    ale's avatar
    ale committed
    	defer tick.Stop()
    
    ale's avatar
    ale committed
    	for {
    		select {
    		case <-tick.C:
    
    ale's avatar
    ale committed
    			upCancel = startUpdate(m.certs)
    
    		case <-testUpdateCh:
    
    ale's avatar
    ale committed
    			upCancel = startUpdate(m.certs)
    
    ale's avatar
    ale committed
    		case certDomains := <-m.configCh:
    
    ale's avatar
    ale committed
    			cancelUpdate()
    			m.certs = m.loadConfig(certDomains)
    
    ale's avatar
    ale committed
    		case <-ctx.Done():
    			return
    		}
    	}
    }
    
    func concatDER(der [][]byte) []byte {
    	// Append DERs to a single []byte buffer and parse the results.
    	var n int
    	for _, b := range der {
    		n += len(b)
    	}
    	out := make([]byte, n)
    	n = 0
    	for _, b := range der {
    		n += copy(out[n:], b)
    	}
    	return out
    }
    
    func certRequest(key crypto.Signer, domains []string) ([]byte, error) {
    	req := &x509.CertificateRequest{
    		Subject: pkix.Name{CommonName: domains[0]},
    	}
    	if len(domains) > 1 {
    		req.DNSNames = domains[1:]
    	}
    	return x509.CreateCertificateRequest(rand.Reader, req, key)
    }
    
    func parsePrivateKey(der []byte) (crypto.Signer, error) {
    	if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
    		return key, nil
    	}
    	if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
    		switch key := key.(type) {
    		case *rsa.PrivateKey:
    			return key, nil
    		case *ecdsa.PrivateKey:
    			return key, nil
    		default:
    			return nil, errors.New("unknown private key type in PKCS#8 wrapping")
    		}
    	}
    	if key, err := x509.ParseECPrivateKey(der); err == nil {
    		return key, nil
    	}
    
    	return nil, errors.New("failed to parse private key")
    }
    
    func encodeECDSAKey(w io.Writer, key *ecdsa.PrivateKey) error {
    	b, err := x509.MarshalECPrivateKey(key)
    	if err != nil {
    		return err
    	}
    	pb := &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
    	return pem.Encode(w, pb)
    }