README

ACMEWrapper

Add Let's Encrypt support to your golang server in 10 lines of code.

GoDoc Go Report Card Build Status

w, err := acmewrapper.New(acmewrapper.Config{
	Domains: []string{"example.com","www.example.com"},
	Address: ":443",

	TLSCertFile: "cert.pem",
	TLSKeyFile:  "key.pem",

	// Let's Encrypt stuff
	RegistrationFile: "user.reg",
	PrivateKeyFile:   "user.pem",

	TOSCallback: acmewrapper.TOSAgree,
})


if err!=nil {
	log.Fatal("acmewrapper: ", err)
}

listener, err := tls.Listen("tcp", ":443", w.TLSConfig())

Acmewrapper is built upon https://github.com/xenolf/lego, and handles all certificate generation, renewal and replacement automatically. After the above code snippet, your certificate will automatically be renewed 30 days before expiring without downtime. Any files that don't exist will be created, and your "cert.pem" and "key.pem" will be kept up to date.

Since Let's Encrypt is usually an option that can be turned off, the wrapper allows disabling ACME support and just using normal certificates, with the bonus of allowing live reload (ie: change your certificates during runtime).

And finally, technically, none of the file names shown above are actually necessary. The only needed fields are Domains and TOSCallback. Without the given file names, acmewrapper runs in-memory. Beware, though: if you do that, you might run into rate limiting from Let's Encrypt if you restart too often!

How It Works

Let's Encrypt has SNI support for domain validation. That means we can update our certificate if we control the TLS configuration of a server. That is exactly what acmewrapper does. Not only does it transparently update your server's certificate, but it uses its control of SNI to pass validation tests.

This means that no other changes are needed to your code. You don't need any special handlers or hidden directories. So long as acmewrapper is able to set your TLS configuration, and your TLS server is running on port 443, you can instantly have a working Let's Encrypt certificate.

Notes

Currently, Go 1.4 and above are supported

  • There was a breaking change on 3/20/16: all time periods in the configuration were switched from int64 to time.Duration.

Example

You can go into ./example to find a sample basic http server that will serve a given folder over https with Let's Encrypt.

Another simple example is given below:

Old Code

This is sample code before adding Let's Encrypt support:

package main

import (
    "io"
    "net/http"
    "log"
)

func HelloServer(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, "hello, world!\n")
}

func main() {
    http.HandleFunc("/hello", HelloServer)
    err := http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}
New Code

Adding let's encrypt support is a matter of setting the tls config:

package main

import (
    "io"
    "net/http"
    "log"
	"crypto/tls"

	"github.com/dkumor/acmewrapper"
)

func HelloServer(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, "hello, world!\n")
}

func main() {
	mux := http.NewServeMux()
    mux.HandleFunc("/hello", HelloServer)

	w, err := acmewrapper.New(acmewrapper.Config{
		Domains: []string{"example.com","www.example.com"},
		Address: ":443",

		TLSCertFile: "cert.pem",
		TLSKeyFile:  "key.pem",

		RegistrationFile: "user.reg",
		PrivateKeyFile:   "user.pem",

		TOSCallback: acmewrapper.TOSAgree,
	})


	if err!=nil {
		log.Fatal("acmewrapper: ", err)
	}

	tlsconfig := w.TLSConfig()

	listener, err := tls.Listen("tcp", ":443", tlsconfig)
    if err != nil {
        log.Fatal("Listener: ", err)
    }

	// To enable http2, we need http.Server to have reference to tlsconfig
	// https://github.com/golang/go/issues/14374
	server := &http.Server{
		Addr: ":443",
		Handler:   mux,
		TLSConfig: tlsconfig,
	}
	server.Serve(listener)
}

Custom File Handlers

While ACMEWrapper saves certificates to the filesystem by default, you can save all relevant files in your database by overloading the read and write functions

w, err := acmewrapper.New(acmewrapper.Config{
		Domains: []string{"example.com","www.example.com"},
		Address: ":443",

		TLSCertFile: "CERTIFICATE",
		TLSKeyFile:  "TLSKEY",

		RegistrationFile: "REGISTRATION",
		PrivateKeyFile:   "PRIVATEKEY",

		TOSCallback: acmewrapper.TOSAgree,

		SaveFileCallback: func(path string, contents []byte) error {
			// the path is the file name as set up in the configuration - the certificate will be "CERTIFICATE", etc.
		},
		// If this callback does not find the file at the provided path, it must return os.ErrNotExist.
		// If this callback returns acmewrapper.ErrNotHandled, it will fallback to load file from disk.
		LoadFileCallback func(path string) (contents []byte, err error) {
			return os.ErrNotExist
		},
	})

Testing

Running the tests is a bit of a chore, since it requires a valid domain name, and access to port 443. This is because ACMEWrapper uses the Let's Encrypt staging server to make sure the code is working.

To test on your own server, you need to change the domain name to your domain, and set a custom testing port that will be routed to 443:

go test -c
sudo setcap cap_net_bind_service=+ep acmewrapper.test
export TLSADDRESS=":443"
export DOMAIN_NAME="example.com"
./acmewrapper.test

Documentation

Index

Constants

View Source
const (
	// DefaultServer is the server to use by default
	DefaultServer = "https://acme-v01.api.letsencrypt.org/directory"
	// DefaultKeyType represents the type for the private key
	DefaultKeyType = acme.RSA2048

	// DefaultAddress is thedefault port to use for initializing certs on startup
	DefaultAddress = ":443"

	// DefaultRenewTime is the time period before cert expiration to attempt renewal
	DefaultRenewTime = 30 * 24 * time.Hour
	// DefaultRetryDelay is the time between renew attempts after a previous attempt failed
	DefaultRetryDelay = 1 * 24 * time.Hour
	// DefaultRenewCheck is the time between checks of the certificate to make sure it doesn't
	// need to be renewed
	DefaultRenewCheck = 12 * time.Hour
)

Variables

View Source
var ErrNotHandled = errors.New("not handled")

    ErrNotHandled is returned by read and write file callbacks if the file should be read from filesystem.

    Functions

    func TOSAgree

    func TOSAgree(agreementURL string) bool

      TOSAgree always agrees to the terms of service. This should only be really used if you realize that you could be selling your soul without being notified.

      func TOSDecline

      func TOSDecline(agreementURL string) bool

        TOSDecline always declines to the terms of service. This can be usd for testing, when you want to make sure that ACME is really off, or that the user is being loaded.

        Types

        type AcmeWrapper

        type AcmeWrapper struct {
        	sync.Mutex // configmutex ensures that settings for the ACME stuff don't happen in parallel
        	Config     Config
        	// contains filtered or unexported fields
        }

          AcmeWrapper is the main object which controls tls certificates and their renewals

          func New

          func New(c Config) (*AcmeWrapper, error)

            New generates an AcmeWrapper given a configuration

            func (*AcmeWrapper) AcmeDisabled

            func (w *AcmeWrapper) AcmeDisabled(set bool) error

              AcmeDisabled allows to enable/disable acme-based certificate. Note that it is assumed that this function is only called during server runtime (ie, your server is already listening). its main purpose is to enable live reload of acme configuration. Do NOT set AcmeDisabled in AcmeWrapper.Config, since it will panic.

              func (*AcmeWrapper) AddSNI

              func (w *AcmeWrapper) AddSNI(domain string, cert *tls.Certificate)

                AddSNI adds a domain name and certificate pair to the AcmeWrapper. Whenever a request is for the passed domain, its associated certifcate is returned.

                func (*AcmeWrapper) CertNeedsUpdate

                func (w *AcmeWrapper) CertNeedsUpdate() bool

                  CertNeedsUpdate returns whether the current certificate either does not exist, or is <X days from expiration, where X is set up in config, or does not match the domains set up in configuration.

                  func (*AcmeWrapper) GetCertificate

                  func (w *AcmeWrapper) GetCertificate() *tls.Certificate

                    GetCertificate returns the current TLS certificate

                    func (*AcmeWrapper) GetEmail

                    func (w *AcmeWrapper) GetEmail() string

                      GetEmail returns the user email (if any) NOTE: NOT threadsafe

                      func (*AcmeWrapper) GetPrivateKey

                      func (w *AcmeWrapper) GetPrivateKey() crypto.PrivateKey

                        GetPrivateKey returns the private key for the given user. NOTE: NOT threadsafe

                        func (*AcmeWrapper) GetRegistration

                        func (w *AcmeWrapper) GetRegistration() *acme.RegistrationResource

                          GetRegistration returns the registration currently being used NOTE: NOT threadsafe

                          func (*AcmeWrapper) RemSNI

                          func (w *AcmeWrapper) RemSNI(domain string)

                            RemSNI removes a domain name and certificate pair from the AcmeWrapper. It is assumed that they were added using AddSNI.

                            func (*AcmeWrapper) Renew

                            func (w *AcmeWrapper) Renew() (err error)

                              Renew generates a new certificate

                              func (*AcmeWrapper) SetNewCert

                              func (w *AcmeWrapper) SetNewCert(certfile, keyfile string) error

                                SetNewCert loads a new TLS key/cert from the given files. Running it with the same filenames as existing cert will reload them

                                func (*AcmeWrapper) TLSConfig

                                func (w *AcmeWrapper) TLSConfig() *tls.Config

                                  TLSConfig returns a TLS configuration that will automatically work with the golang ssl listener. This sets it up so that the server automatically uses a working cert, and updates the cert when necessary.

                                  func (*AcmeWrapper) TLSConfigGetCertificate

                                  func (w *AcmeWrapper) TLSConfigGetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error)

                                    TLSConfigGetCertificate is the main function used in the ACME wrapper. This is set in tls.Config to the GetCertificate property. Note that Certificates must be empty for it to be called correctly, so unless you know what you're doing, just use AcmeWrapper.TLSConfig()

                                    type Config

                                    type Config struct {
                                    	// The ACME server to query for key/cert. Default (Let's Encrypt) used if not set
                                    	Server string
                                    
                                    	// The domain for which to generate your certificate. Suppose you own mysite.com.
                                    	// The domains to pass in are Domains: []string{"mysite.com","www.mysite.com"}. Don't
                                    	// forget about the www version of your domain.
                                    	Domains []string
                                    
                                    	// The file to read/write the private key from/to. If this is not empty, and the file does not exist,
                                    	// then the user is assumed not to be registered, and the file is created. if this is empty, then
                                    	// a new private key is generated and used for all queries. The private key is lost on stopping the program.
                                    	PrivateKeyFile string
                                    	PrivateKeyType acme.KeyType // The private key type. Default is 2048 (RSA)
                                    
                                    	// The file to read/write registration info to. The ACME protocol requires remembering some details
                                    	// about a registration. Therefore, the file is saved at the given location.
                                    	// If not given, and PrivateKeyFile is given, then gives an error - if you're saving your private key,
                                    	// you need to save your user registration.
                                    	RegistrationFile string
                                    
                                    	Email string `json:"email"` // Optional user email
                                    
                                    	// File names at which to read/write the TLS key and certificate. These are optional. If there
                                    	// is no file given, then the keys are kept in memory. NOTE: You need write access to these files,
                                    	// since they are overwritten each time a new certificate is requested.
                                    	// Also, it is HIGHLY recommended that you save the files, since Let's Encrypt has fairly low limits
                                    	// for how often certs for the same site can be requested (5/week at the time of writing).
                                    	TLSCertFile string
                                    	TLSKeyFile  string
                                    
                                    	RenewTime  time.Duration // The time in seconds until expiration of current cert that renew is attempted. If not set, default is 30d
                                    	RetryDelay time.Duration // The time in seconds to delay between attempts at renewing if renewal fails. (1 day)
                                    	RenewCheck time.Duration // The time between checks for renewal. Default is 12h
                                    
                                    	// The callback to use prompting the user to agree to the terms of service. A special Agree is built in, so
                                    	// you can set TOSCallback: TOSAgree
                                    	TOSCallback TOSCallback
                                    
                                    	// If there is no certificate set up at all, we need to generate an initial one
                                    	// to jump-start the server. Therefore, you should input the port that you
                                    	// will use when running listen. If there are no certs, it runs a temporary mini
                                    	// server at that location to generate initial certificates. Once that is done,
                                    	// all further renewals are done through the SNI interface to your own server code.
                                    	// The default here is 443
                                    	Address string
                                    
                                    	// This callback is run before each attempt at renewing. If not set, it simply isn't run.
                                    	RenewCallback func()
                                    	// RenewFailedCallback is run if renewing failed.
                                    	RenewFailedCallback func(error)
                                    
                                    	// When this is set to True, no ACME-related things happen - it just passes through your
                                    	// key and cert directly.
                                    	AcmeDisabled bool
                                    
                                    	// When this callback is defined, it will be used to save all files.
                                    	// If this callback returns acmewrapper.ErrNotHandled, it will fallback to save file to disk.
                                    	SaveFileCallback func(path string, contents []byte) error
                                    	// When this callback is defined, it will be used to load all files.
                                    	// If this callback does not find the file at the provided path, it must return os.ErrNotExist.
                                    	// If this callback returns acmewrapper.ErrNotHandled, it will fallback to load file from disk.
                                    	LoadFileCallback func(path string) (contents []byte, err error)
                                    }

                                      Config is the setup to use for generating your TLS keys. While the only required component is Server, it is recommended that you save at least your TLS cert and key

                                      type LoggerInterface

                                      type LoggerInterface interface {
                                      	Printf(format string, v ...interface{})
                                      }

                                        LoggerInterface represents anything that can Printf.

                                        var Logger LoggerInterface

                                          Logger allows to use a custom logger for logging purposes

                                          type TOSCallback

                                          type TOSCallback func(agreementURL string) bool

                                            TOSCallback is a callback to run when the TOS have changed, and need to be agreed to. The returned bool is agree/not agree

                                            Directories

                                            Path Synopsis