github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/pkg/acme/acme.go (about)

     1  // Package acme provides a means of performing Let's Encrypt DNS challenges via a DNSConfig
     2  package acme
     3  
     4  import (
     5  	"crypto/x509"
     6  	"encoding/pem"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"log"
    10  	"net/url"
    11  	"sort"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/StackExchange/dnscontrol/v2/models"
    16  	"github.com/StackExchange/dnscontrol/v2/pkg/nameservers"
    17  	"github.com/StackExchange/dnscontrol/v2/pkg/notifications"
    18  	"github.com/go-acme/lego/certcrypto"
    19  	"github.com/go-acme/lego/certificate"
    20  	"github.com/go-acme/lego/challenge"
    21  	"github.com/go-acme/lego/challenge/dns01"
    22  	"github.com/go-acme/lego/lego"
    23  	acmelog "github.com/go-acme/lego/log"
    24  )
    25  
    26  type CertConfig struct {
    27  	CertName   string   `json:"cert_name"`
    28  	Names      []string `json:"names"`
    29  	UseECC     bool     `json:"use_ecc"`
    30  	MustStaple bool     `json:"must_staple"`
    31  }
    32  
    33  type Client interface {
    34  	IssueOrRenewCert(config *CertConfig, renewUnder int, verbose bool) (bool, error)
    35  }
    36  
    37  type certManager struct {
    38  	email         string
    39  	acmeDirectory string
    40  	acmeHost      string
    41  
    42  	storage         Storage
    43  	cfg             *models.DNSConfig
    44  	domains         map[string]*models.DomainConfig
    45  	originalDomains []*models.DomainConfig
    46  
    47  	notifier notifications.Notifier
    48  
    49  	account    *Account
    50  	waitedOnce bool
    51  }
    52  
    53  const (
    54  	LetsEncryptLive  = "https://acme-v02.api.letsencrypt.org/directory"
    55  	LetsEncryptStage = "https://acme-staging-v02.api.letsencrypt.org/directory"
    56  )
    57  
    58  func New(cfg *models.DNSConfig, directory string, email string, server string, notify notifications.Notifier) (Client, error) {
    59  	return commonNew(cfg, directoryStorage(directory), email, server, notify)
    60  }
    61  
    62  func commonNew(cfg *models.DNSConfig, storage Storage, email string, server string, notify notifications.Notifier) (Client, error) {
    63  	u, err := url.Parse(server)
    64  	if err != nil || u.Host == "" {
    65  		return nil, fmt.Errorf("ACME directory '%s' is not a valid URL", server)
    66  	}
    67  	c := &certManager{
    68  		storage:       storage,
    69  		email:         email,
    70  		acmeDirectory: server,
    71  		acmeHost:      u.Host,
    72  		cfg:           cfg,
    73  		domains:       map[string]*models.DomainConfig{},
    74  		notifier:      notify,
    75  	}
    76  
    77  	acct, err := c.getOrCreateAccount()
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	c.account = acct
    82  	return c, nil
    83  }
    84  
    85  func NewVault(cfg *models.DNSConfig, vaultPath string, email string, server string, notify notifications.Notifier) (Client, error) {
    86  	storage, err := makeVaultStorage(vaultPath)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	return commonNew(cfg, storage, email, server, notify)
    91  }
    92  
    93  // IssueOrRenewCert will obtain a certificate with the given name if it does not exist,
    94  // or renew it if it is close enough to the expiration date.
    95  // It will return true if it issued or updated the certificate.
    96  func (c *certManager) IssueOrRenewCert(cfg *CertConfig, renewUnder int, verbose bool) (bool, error) {
    97  	if !verbose {
    98  		acmelog.Logger = log.New(ioutil.Discard, "", 0)
    99  	}
   100  	defer c.finalCleanUp()
   101  
   102  	log.Printf("Checking certificate [%s]", cfg.CertName)
   103  	existing, err := c.storage.GetCertificate(cfg.CertName)
   104  	if err != nil {
   105  		return false, err
   106  	}
   107  
   108  	var client *lego.Client
   109  
   110  	var action = func() (*certificate.Resource, error) {
   111  		return client.Certificate.Obtain(certificate.ObtainRequest{
   112  			Bundle:     true,
   113  			Domains:    cfg.Names,
   114  			MustStaple: cfg.MustStaple,
   115  		})
   116  	}
   117  
   118  	if existing == nil {
   119  		log.Println("No existing cert found. Issuing new...")
   120  	} else {
   121  		names, daysLeft, err := getCertInfo(existing.Certificate)
   122  		if err != nil {
   123  			return false, err
   124  		}
   125  		log.Printf("Found existing cert. %0.2f days remaining.", daysLeft)
   126  		namesOK := dnsNamesEqual(cfg.Names, names)
   127  		if daysLeft >= float64(renewUnder) && namesOK {
   128  			log.Println("Nothing to do")
   129  			//nothing to do
   130  			return false, nil
   131  		}
   132  		if !namesOK {
   133  			log.Println("DNS Names don't match expected set. Reissuing.")
   134  		} else {
   135  			log.Println("Renewing cert")
   136  			action = func() (*certificate.Resource, error) {
   137  				return client.Certificate.Renew(*existing, true, cfg.MustStaple)
   138  			}
   139  		}
   140  	}
   141  
   142  	kt := certcrypto.RSA2048
   143  	if cfg.UseECC {
   144  		kt = certcrypto.EC256
   145  	}
   146  	config := lego.NewConfig(c.account)
   147  	config.CADirURL = c.acmeDirectory
   148  	config.Certificate.KeyType = kt
   149  	client, err = lego.NewClient(config)
   150  	if err != nil {
   151  		return false, err
   152  	}
   153  	client.Challenge.Remove(challenge.HTTP01)
   154  	client.Challenge.Remove(challenge.TLSALPN01)
   155  	client.Challenge.SetDNS01Provider(c, dns01.WrapPreCheck(c.preCheckDNS))
   156  
   157  	certResource, err := action()
   158  	if err != nil {
   159  		return false, err
   160  	}
   161  	fmt.Printf("Obtained certificate for %s\n", cfg.CertName)
   162  	if err = c.storage.StoreCertificate(cfg.CertName, certResource); err != nil {
   163  		return true, err
   164  	}
   165  
   166  	return true, nil
   167  }
   168  
   169  func getCertInfo(pemBytes []byte) (names []string, remaining float64, err error) {
   170  	block, _ := pem.Decode(pemBytes)
   171  	if block == nil {
   172  		return nil, 0, fmt.Errorf("Invalid certificate pem data")
   173  	}
   174  	cert, err := x509.ParseCertificate(block.Bytes)
   175  	if err != nil {
   176  		return nil, 0, err
   177  	}
   178  	var daysLeft = float64(cert.NotAfter.Sub(time.Now())) / float64(time.Hour*24)
   179  	return cert.DNSNames, daysLeft, nil
   180  }
   181  
   182  // checks two lists of sans to make sure they have all the same names in them.
   183  func dnsNamesEqual(a []string, b []string) bool {
   184  	if len(a) != len(b) {
   185  		return false
   186  	}
   187  	sort.Strings(a)
   188  	sort.Strings(b)
   189  	for i, s := range a {
   190  		if b[i] != s {
   191  			return false
   192  		}
   193  	}
   194  	return true
   195  }
   196  
   197  func (c *certManager) Present(domain, token, keyAuth string) (e error) {
   198  	d := c.cfg.DomainContainingFQDN(domain)
   199  	name := d.Name
   200  	if seen := c.domains[name]; seen != nil {
   201  		// we've already pre-processed this domain, just need to add to it.
   202  		d = seen
   203  	} else {
   204  		// one-time tasks to get this domain ready.
   205  		// if multiple validations on a single domain, we don't need to rebuild all this.
   206  
   207  		// fix NS records for this domain's DNS providers
   208  		nsList, err := nameservers.DetermineNameservers(d)
   209  		if err != nil {
   210  			return err
   211  		}
   212  		d.Nameservers = nsList
   213  		nameservers.AddNSRecords(d)
   214  
   215  		// make sure we have the latest config before we change anything.
   216  		// alternately, we could avoid a lot of this trouble if we really really trusted no-purge in all cases
   217  		if err := c.ensureNoPendingCorrections(d); err != nil {
   218  			return err
   219  		}
   220  
   221  		// copy domain and work from copy from now on. That way original config can be used to "restore" when we are all done.
   222  		copy, err := d.Copy()
   223  		if err != nil {
   224  			return err
   225  		}
   226  		c.originalDomains = append(c.originalDomains, d)
   227  		c.domains[name] = copy
   228  		d = copy
   229  	}
   230  
   231  	fqdn, val := dns01.GetRecord(domain, keyAuth)
   232  	txt := &models.RecordConfig{Type: "TXT"}
   233  	txt.SetTargetTXT(val)
   234  	txt.SetLabelFromFQDN(fqdn, d.Name)
   235  	d.Records = append(d.Records, txt)
   236  	return c.getAndRunCorrections(d)
   237  }
   238  
   239  func (c *certManager) ensureNoPendingCorrections(d *models.DomainConfig) error {
   240  	corrections, err := c.getCorrections(d)
   241  	if err != nil {
   242  		return err
   243  	}
   244  	if len(corrections) != 0 {
   245  		// TODO: maybe allow forcing through this check.
   246  		for _, c := range corrections {
   247  			fmt.Println(c.Msg)
   248  		}
   249  		return fmt.Errorf("Found %d pending corrections for %s. Not going to proceed issuing certificates", len(corrections), d.Name)
   250  	}
   251  	return nil
   252  }
   253  
   254  // IgnoredProviders is a lit of provider names that should not be used to fill challenges.
   255  var IgnoredProviders = map[string]bool{}
   256  
   257  func (c *certManager) getCorrections(d *models.DomainConfig) ([]*models.Correction, error) {
   258  	cs := []*models.Correction{}
   259  	for _, p := range d.DNSProviderInstances {
   260  		if IgnoredProviders[p.Name] {
   261  			continue
   262  		}
   263  		dc, err := d.Copy()
   264  		if err != nil {
   265  			return nil, err
   266  		}
   267  		corrections, err := p.Driver.GetDomainCorrections(dc)
   268  		if err != nil {
   269  			return nil, err
   270  		}
   271  		for _, c := range corrections {
   272  			c.Msg = fmt.Sprintf("[%s] %s", p.Name, strings.TrimSpace(c.Msg))
   273  		}
   274  		cs = append(cs, corrections...)
   275  	}
   276  	return cs, nil
   277  }
   278  
   279  func (c *certManager) getAndRunCorrections(d *models.DomainConfig) error {
   280  	cs, err := c.getCorrections(d)
   281  	if err != nil {
   282  		return err
   283  	}
   284  	fmt.Printf("%d corrections\n", len(cs))
   285  	for _, corr := range cs {
   286  		fmt.Printf("Running [%s]\n", corr.Msg)
   287  		err = corr.F()
   288  		c.notifier.Notify(d.Name, "certs", corr.Msg, err, false)
   289  		if err != nil {
   290  			return err
   291  		}
   292  	}
   293  	return nil
   294  }
   295  
   296  func (c *certManager) CleanUp(domain, token, keyAuth string) error {
   297  	// do nothing for now. We will do a final clean up step at the very end.
   298  	return nil
   299  }
   300  
   301  func (c *certManager) finalCleanUp() error {
   302  	log.Println("Cleaning up all records we made")
   303  	var lastError error
   304  	for _, d := range c.originalDomains {
   305  		if err := c.getAndRunCorrections(d); err != nil {
   306  			log.Printf("ERROR cleaning up: %s", err)
   307  			lastError = err
   308  		}
   309  	}
   310  	return lastError
   311  }