Skip to content
Snippets Groups Projects
manager.go 7.58 KiB
Newer Older
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/tools/replds"
ale's avatar
ale committed
	"github.com/prometheus/client_golang/prometheus"
ale's avatar
ale committed
)

var (
	defaultRenewalDays = 21
	updateInterval     = 1 * time.Minute
)

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.
ale's avatar
ale committed
type Manager struct {
ale's avatar
ale committed
	configDirs  []string
ale's avatar
ale committed
	useRSA      bool
	storage     certStorage
	certGen     CertGenerator
	renewalDays int

	configCh chan []*certConfig
ale's avatar
ale committed
	doneCh   chan bool

	mx    sync.Mutex
	certs []*certInfo
ale's avatar
ale committed
}

// 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
	}
	if config.RenewalDays <= 0 {
		config.RenewalDays = defaultRenewalDays
	}
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,
	}
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 = 6 * time.Hour
ale's avatar
ale committed
)

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
}

func (m *Manager) getCerts() []*certInfo {
	m.mx.Lock()
	defer m.mx.Unlock()
	return m.certs
}

func (m *Manager) setCerts(certs []*certInfo) {
	m.mx.Lock()
	m.certs = certs
	m.mx.Unlock()
}

// 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) {
	reloadCh := make(chan interface{}, 1)
	go func() {
		for config := range m.configCh {
			certs := m.loadConfig(config)
			m.setCerts(certs)
			reloadCh <- certs
ale's avatar
ale committed
		}
ale's avatar
ale committed

	runWithUpdates(
		ctx,
		func(ctx context.Context, value interface{}) {
			certs := value.([]*certInfo)
			m.updateAllCerts(ctx, certs)
		},
		reloadCh,
		updateInterval,
	)
ale's avatar
ale committed
}

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]},
		DNSNames: domains,
ale's avatar
ale committed
	}
	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)
}