| 
									
										
										
										
											2016-03-18 20:57:35 +00:00
										 |  |  | // Package https facilitates the management of TLS assets and integrates | 
					
						
							|  |  |  | // Let's Encrypt functionality into Caddy with first-class support for | 
					
						
							|  |  |  | // creating and renewing certificates automatically. It is designed to | 
					
						
							|  |  |  | // configure sites for HTTPS by default. | 
					
						
							|  |  |  | package https | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"encoding/json" | 
					
						
							|  |  |  | 	"errors" | 
					
						
							|  |  |  | 	"io/ioutil" | 
					
						
							|  |  |  | 	"net" | 
					
						
							|  |  |  | 	"os" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/miekg/coredns/server" | 
					
						
							|  |  |  | 	"github.com/xenolf/lego/acme" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Activate sets up TLS for each server config in configs | 
					
						
							|  |  |  | // as needed; this consists of acquiring and maintaining | 
					
						
							|  |  |  | // certificates and keys for qualifying configs and enabling | 
					
						
							|  |  |  | // OCSP stapling for all TLS-enabled configs. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // This function may prompt the user to provide an email | 
					
						
							|  |  |  | // address if none is available through other means. It | 
					
						
							|  |  |  | // prefers the email address specified in the config, but | 
					
						
							|  |  |  | // if that is not available it will check the command line | 
					
						
							|  |  |  | // argument. If absent, it will use the most recent email | 
					
						
							|  |  |  | // address from last time. If there isn't one, the user | 
					
						
							|  |  |  | // will be prompted and shown SA link. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // Also note that calling this function activates asset | 
					
						
							|  |  |  | // management automatically, which keeps certificates | 
					
						
							|  |  |  | // renewed and OCSP stapling updated. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // Activate returns the updated list of configs, since | 
					
						
							|  |  |  | // some may have been appended, for example, to redirect | 
					
						
							|  |  |  | // plaintext HTTP requests to their HTTPS counterpart. | 
					
						
							|  |  |  | // This function only appends; it does not splice. | 
					
						
							|  |  |  | func Activate(configs []server.Config) ([]server.Config, error) { | 
					
						
							|  |  |  | 	// just in case previous caller forgot... | 
					
						
							|  |  |  | 	Deactivate() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// pre-screen each config and earmark the ones that qualify for managed TLS | 
					
						
							|  |  |  | 	MarkQualified(configs) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// place certificates and keys on disk | 
					
						
							|  |  |  | 	err := ObtainCerts(configs, true, false) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return configs, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// update TLS configurations | 
					
						
							|  |  |  | 	err = EnableTLS(configs, true) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return configs, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// renew all relevant certificates that need renewal. this is important | 
					
						
							|  |  |  | 	// to do right away for a couple reasons, mainly because each restart, | 
					
						
							|  |  |  | 	// the renewal ticker is reset, so if restarts happen more often than | 
					
						
							|  |  |  | 	// the ticker interval, renewals would never happen. but doing | 
					
						
							|  |  |  | 	// it right away at start guarantees that renewals aren't missed. | 
					
						
							|  |  |  | 	err = renewManagedCertificates(true) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return configs, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// keep certificates renewed and OCSP stapling updated | 
					
						
							|  |  |  | 	go maintainAssets(stopChan) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return configs, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Deactivate cleans up long-term, in-memory resources | 
					
						
							|  |  |  | // allocated by calling Activate(). Essentially, it stops | 
					
						
							|  |  |  | // the asset maintainer from running, meaning that certificates | 
					
						
							|  |  |  | // will not be renewed, OCSP staples will not be updated, etc. | 
					
						
							|  |  |  | func Deactivate() (err error) { | 
					
						
							|  |  |  | 	defer func() { | 
					
						
							|  |  |  | 		if rec := recover(); rec != nil { | 
					
						
							|  |  |  | 			err = errors.New("already deactivated") | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	}() | 
					
						
							|  |  |  | 	close(stopChan) | 
					
						
							|  |  |  | 	stopChan = make(chan struct{}) | 
					
						
							|  |  |  | 	return | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // MarkQualified scans each config and, if it qualifies for managed | 
					
						
							|  |  |  | // TLS, it sets the Managed field of the TLSConfig to true. | 
					
						
							|  |  |  | func MarkQualified(configs []server.Config) { | 
					
						
							|  |  |  | 	for i := 0; i < len(configs); i++ { | 
					
						
							|  |  |  | 		if ConfigQualifies(configs[i]) { | 
					
						
							|  |  |  | 			configs[i].TLS.Managed = true | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // ObtainCerts obtains certificates for all these configs as long as a | 
					
						
							|  |  |  | // certificate does not already exist on disk. It does not modify the | 
					
						
							|  |  |  | // configs at all; it only obtains and stores certificates and keys to | 
					
						
							|  |  |  | // the disk. If allowPrompts is true, the user may be shown a prompt. | 
					
						
							|  |  |  | // If proxyACME is true, the ACME challenges will be proxied to our alt port. | 
					
						
							|  |  |  | func ObtainCerts(configs []server.Config, allowPrompts, proxyACME bool) error { | 
					
						
							|  |  |  | 	// We group configs by email so we don't make the same clients over and | 
					
						
							|  |  |  | 	// over. This has the potential to prompt the user for an email, but we | 
					
						
							|  |  |  | 	// prevent that by assuming that if we already have a listener that can | 
					
						
							|  |  |  | 	// proxy ACME challenge requests, then the server is already running and | 
					
						
							|  |  |  | 	// the operator is no longer present. | 
					
						
							|  |  |  | 	groupedConfigs := groupConfigsByEmail(configs, allowPrompts) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for email, group := range groupedConfigs { | 
					
						
							|  |  |  | 		// Wait as long as we can before creating the client, because it | 
					
						
							|  |  |  | 		// may not be needed, for example, if we already have what we | 
					
						
							|  |  |  | 		// need on disk. Creating a client involves the network and | 
					
						
							|  |  |  | 		// potentially prompting the user, etc., so only do if necessary. | 
					
						
							|  |  |  | 		var client *ACMEClient | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		for _, cfg := range group { | 
					
						
							| 
									
										
										
										
											2016-03-19 14:46:32 +00:00
										 |  |  | 			if existingCertAndKey(cfg.Host) { | 
					
						
							| 
									
										
										
										
											2016-03-18 20:57:35 +00:00
										 |  |  | 				continue | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Now we definitely do need a client | 
					
						
							|  |  |  | 			if client == nil { | 
					
						
							|  |  |  | 				var err error | 
					
						
							|  |  |  | 				client, err = NewACMEClient(email, allowPrompts) | 
					
						
							|  |  |  | 				if err != nil { | 
					
						
							|  |  |  | 					return errors.New("error creating client: " + err.Error()) | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// c.Configure assumes that allowPrompts == !proxyACME, | 
					
						
							|  |  |  | 			// but that's not always true. For example, a restart where | 
					
						
							|  |  |  | 			// the user isn't present and we're not listening on port 80. | 
					
						
							|  |  |  | 			// TODO: This could probably be refactored better. | 
					
						
							|  |  |  | 			if proxyACME { | 
					
						
							|  |  |  | 				client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, AlternatePort)) | 
					
						
							|  |  |  | 				client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, AlternatePort)) | 
					
						
							|  |  |  | 				client.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01}) | 
					
						
							|  |  |  | 			} else { | 
					
						
							|  |  |  | 				client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, "")) | 
					
						
							|  |  |  | 				client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, "")) | 
					
						
							|  |  |  | 				client.ExcludeChallenges([]acme.Challenge{acme.DNS01}) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			err := client.Obtain([]string{cfg.Host}) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				return err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // groupConfigsByEmail groups configs by the email address to be used by an | 
					
						
							|  |  |  | // ACME client. It only groups configs that have TLS enabled and that are | 
					
						
							|  |  |  | // marked as Managed. If userPresent is true, the operator MAY be prompted | 
					
						
							|  |  |  | // for an email address. | 
					
						
							|  |  |  | func groupConfigsByEmail(configs []server.Config, userPresent bool) map[string][]server.Config { | 
					
						
							|  |  |  | 	initMap := make(map[string][]server.Config) | 
					
						
							|  |  |  | 	for _, cfg := range configs { | 
					
						
							|  |  |  | 		if !cfg.TLS.Managed { | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		leEmail := getEmail(cfg, userPresent) | 
					
						
							|  |  |  | 		initMap[leEmail] = append(initMap[leEmail], cfg) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return initMap | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // EnableTLS configures each config to use TLS according to default settings. | 
					
						
							|  |  |  | // It will only change configs that are marked as managed, and assumes that | 
					
						
							|  |  |  | // certificates and keys are already on disk. If loadCertificates is true, | 
					
						
							|  |  |  | // the certificates will be loaded from disk into the cache for this process | 
					
						
							|  |  |  | // to use. If false, TLS will still be enabled and configured with default | 
					
						
							|  |  |  | // settings, but no certificates will be parsed loaded into the cache, and | 
					
						
							|  |  |  | // the returned error value will always be nil. | 
					
						
							|  |  |  | func EnableTLS(configs []server.Config, loadCertificates bool) error { | 
					
						
							|  |  |  | 	for i := 0; i < len(configs); i++ { | 
					
						
							|  |  |  | 		if !configs[i].TLS.Managed { | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		configs[i].TLS.Enabled = true | 
					
						
							| 
									
										
										
										
											2016-03-19 14:46:32 +00:00
										 |  |  | 		if loadCertificates { | 
					
						
							| 
									
										
										
										
											2016-03-18 20:57:35 +00:00
										 |  |  | 			_, err := cacheManagedCertificate(configs[i].Host, false) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				return err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		setDefaultTLSParams(&configs[i]) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // hostHasOtherPort returns true if there is another config in the list with the same | 
					
						
							|  |  |  | // hostname that has port otherPort, or false otherwise. All the configs are checked | 
					
						
							|  |  |  | // against the hostname of allConfigs[thisConfigIdx]. | 
					
						
							|  |  |  | func hostHasOtherPort(allConfigs []server.Config, thisConfigIdx int, otherPort string) bool { | 
					
						
							|  |  |  | 	for i, otherCfg := range allConfigs { | 
					
						
							|  |  |  | 		if i == thisConfigIdx { | 
					
						
							|  |  |  | 			continue // has to be a config OTHER than the one we're comparing against | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		if otherCfg.Host == allConfigs[thisConfigIdx].Host && otherCfg.Port == otherPort { | 
					
						
							|  |  |  | 			return true | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return false | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // ConfigQualifies returns true if cfg qualifies for | 
					
						
							|  |  |  | // fully managed TLS (but not on-demand TLS, which is | 
					
						
							|  |  |  | // not considered here). It does NOT check to see if a | 
					
						
							|  |  |  | // cert and key already exist for the config. If the | 
					
						
							|  |  |  | // config does qualify, you should set cfg.TLS.Managed | 
					
						
							|  |  |  | // to true and check that instead, because the process of | 
					
						
							|  |  |  | // setting up the config may make it look like it | 
					
						
							|  |  |  | // doesn't qualify even though it originally did. | 
					
						
							|  |  |  | func ConfigQualifies(cfg server.Config) bool { | 
					
						
							|  |  |  | 	return (!cfg.TLS.Manual || cfg.TLS.OnDemand) && // user might provide own cert and key | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// user can force-disable automatic HTTPS for this host | 
					
						
							|  |  |  | 		cfg.Port != "80" && | 
					
						
							|  |  |  | 		cfg.TLS.LetsEncryptEmail != "off" && | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// we get can't certs for some kinds of hostnames, but | 
					
						
							|  |  |  | 		// on-demand TLS allows empty hostnames at startup | 
					
						
							| 
									
										
										
										
											2016-03-19 14:46:32 +00:00
										 |  |  | 		cfg.TLS.OnDemand | 
					
						
							| 
									
										
										
										
											2016-03-18 20:57:35 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // existingCertAndKey returns true if the host has a certificate | 
					
						
							|  |  |  | // and private key in storage already, false otherwise. | 
					
						
							|  |  |  | func existingCertAndKey(host string) bool { | 
					
						
							|  |  |  | 	_, err := os.Stat(storage.SiteCertFile(host)) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return false | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	_, err = os.Stat(storage.SiteKeyFile(host)) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return false | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return true | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // saveCertResource saves the certificate resource to disk. This | 
					
						
							|  |  |  | // includes the certificate file itself, the private key, and the | 
					
						
							|  |  |  | // metadata file. | 
					
						
							|  |  |  | func saveCertResource(cert acme.CertificateResource) error { | 
					
						
							|  |  |  | 	err := os.MkdirAll(storage.Site(cert.Domain), 0700) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Save cert | 
					
						
							|  |  |  | 	err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Save private key | 
					
						
							|  |  |  | 	err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Save cert metadata | 
					
						
							|  |  |  | 	jsonBytes, err := json.MarshalIndent(&cert, "", "\t") | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Revoke revokes the certificate for host via ACME protocol. | 
					
						
							|  |  |  | func Revoke(host string) error { | 
					
						
							|  |  |  | 	if !existingCertAndKey(host) { | 
					
						
							|  |  |  | 		return errors.New("no certificate and key for " + host) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	email := getEmail(server.Config{Host: host}, true) | 
					
						
							|  |  |  | 	if email == "" { | 
					
						
							|  |  |  | 		return errors.New("email is required to revoke") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	client, err := NewACMEClient(email, true) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	certFile := storage.SiteCertFile(host) | 
					
						
							|  |  |  | 	certBytes, err := ioutil.ReadFile(certFile) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	err = client.RevokeCertificate(certBytes) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	err = os.Remove(certFile) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return errors.New("certificate revoked, but unable to delete certificate file: " + err.Error()) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var ( | 
					
						
							|  |  |  | 	// DefaultEmail represents the Let's Encrypt account email to use if none provided | 
					
						
							|  |  |  | 	DefaultEmail string | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Agreed indicates whether user has agreed to the Let's Encrypt SA | 
					
						
							|  |  |  | 	Agreed bool | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// CAUrl represents the base URL to the CA's ACME endpoint | 
					
						
							|  |  |  | 	CAUrl string | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // AlternatePort is the port on which the acme client will open a | 
					
						
							|  |  |  | // listener and solve the CA's challenges. If this alternate port | 
					
						
							|  |  |  | // is used instead of the default port (80 or 443), then the | 
					
						
							|  |  |  | // default port for the challenge must be forwarded to this one. | 
					
						
							|  |  |  | const AlternatePort = "5033" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // KeyType is the type to use for new keys. | 
					
						
							|  |  |  | // This shouldn't need to change except for in tests; | 
					
						
							|  |  |  | // the size can be drastically reduced for speed. | 
					
						
							|  |  |  | var KeyType = acme.EC384 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // stopChan is used to signal the maintenance goroutine | 
					
						
							|  |  |  | // to terminate. | 
					
						
							|  |  |  | var stopChan chan struct{} |