├── example ├── www │ └── index.html ├── README.md └── example.go ├── .travis.yml ├── .gitignore ├── setup_test.go ├── CONTRIBUTING.md ├── rw_callback_test.go ├── LICENSE ├── rw.go ├── challengeprovider.go ├── rwkey.go ├── acme_test.go ├── cert.go ├── renew.go ├── configuration.go ├── cert_test.go ├── acme.go ├── README.md └── acmewrapper.go /example/www/index.html: -------------------------------------------------------------------------------- 1 |

2 | It works! 3 |

4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.7 4 | - 1.6 5 | - 1.5 6 | - 1.4 7 | script: 8 | - go build -o example/example example/example.go 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | *.key 27 | *.crt 28 | *.pem 29 | *.reg 30 | *.bak 31 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | This is a very basic server which shows how to use acmewrapper 4 | 5 | ``` 6 | go build 7 | ``` 8 | 9 | will give you the executable 10 | 11 | ## Running 12 | 13 | The server will serve the directory you specify as its last argument: 14 | 15 | ``` 16 | ./example -accept -host=:8443 mywebsite.com www.mywebsite.com ./www 17 | ``` 18 | 19 | The above will run a server at port 8443 (which 443 is assumed to forward to), with certs for example.com and www.example.com. 20 | 21 | It will serve the www directory. 22 | 23 | Note that when testing, you should use the `-test` flag to make sure you are not generating valid certificates (you might run into the limits accidentally) 24 | -------------------------------------------------------------------------------- /setup_test.go: -------------------------------------------------------------------------------- 1 | package acmewrapper 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | const TESTAPI = "https://acme-staging.api.letsencrypt.org/directory" 10 | 11 | var TESTDOMAINS []string 12 | var TLSADDRESS string 13 | 14 | func TestMain(m *testing.M) { 15 | 16 | tlsaddr := os.Getenv("TLSADDRESS") 17 | if tlsaddr == "" { 18 | tlsaddr = ":443" 19 | } 20 | // Set up the domain to use for tests 21 | dom := os.Getenv("DOMAIN_NAME") 22 | if dom == "" { 23 | fmt.Printf("NO DOMAIN SET\n\tSet a valid testing domain name\n\tin the DOMAIN_NAME environmental variable:\n\n\t\texport DOMAIN_NAME=\"example.com\"\n\n") 24 | os.Exit(-1) 25 | } 26 | fmt.Printf("USING DOMAIN_NAME='%s'\nUSING TLSADDRESS='%s'\n", dom, tlsaddr) 27 | TESTDOMAINS = []string{dom} 28 | TLSADDRESS = tlsaddr 29 | 30 | retCode := m.Run() 31 | 32 | // call with result of m.Run() 33 | os.Exit(retCode) 34 | } 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ACMEWrapper 2 | 3 | ## Bug Reports 4 | 5 | Please check that your issue was not yet reported. Make sure to check in closed issues (use issue search) to see if it was already solved. 6 | 7 | If you do not find your issue, create a new one and include all information necessary to reproduce your problem. You should include enough information for someone to be able to reproduce your problem without the need for followup. 8 | 9 | ## Pull Requests 10 | 11 | Patches, new features, and improvements are a great way to help the project. Make sure that your pull request focuses on one feature and does not include extraneous commits. 12 | 13 | If you make significant changes to the code, make sure to include tests! 14 | 15 | ## License 16 | 17 | By contributing to ACMEWrapper, you agree to have the contribution licensed under the MIT license, and claim that by doing so you are not violating any laws/contracts/copyrights, etc. 18 | -------------------------------------------------------------------------------- /rw_callback_test.go: -------------------------------------------------------------------------------- 1 | package acmewrapper 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestSaveLoadCallback(t *testing.T) { 11 | memFS := map[string][]byte{} 12 | _, err := New(Config{ 13 | Server: TESTAPI, 14 | TOSCallback: TOSAgree, 15 | Domains: TESTDOMAINS, 16 | PrivateKeyFile: "testinguser.key", 17 | RegistrationFile: "testinguser.reg", 18 | Address: TLSADDRESS, 19 | TLSCertFile: "cert.crt", 20 | TLSKeyFile: "key.pem", 21 | SaveFileCallback: func(path string, contents []byte) error { 22 | memFS[path] = contents 23 | return nil 24 | }, 25 | LoadFileCallback: func(path string) ([]byte, error) { 26 | contents, ok := memFS[path] 27 | if !ok { 28 | return nil, os.ErrNotExist 29 | } 30 | return contents, nil 31 | }, 32 | }) 33 | require.NoError(t, err) 34 | require.Len(t, memFS, 4) 35 | require.NotNil(t, memFS["testinguser.key"]) 36 | require.NotNil(t, memFS["testinguser.reg"]) 37 | require.NotNil(t, memFS["cert.crt"]) 38 | require.NotNil(t, memFS["key.pem"]) 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Daniel Kumor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rw.go: -------------------------------------------------------------------------------- 1 | package acmewrapper 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | ) 7 | 8 | // ErrNotHandled is returned by read and write file callbacks if the file should be 9 | // read from filesystem. 10 | var ErrNotHandled = errors.New("not handled") 11 | 12 | func (w *AcmeWrapper) loadFile(path string) ([]byte, error) { 13 | //use custom load file callback? 14 | if w.Config.LoadFileCallback != nil { 15 | if b, err := w.Config.LoadFileCallback(path); err == nil { 16 | return b, nil 17 | } else if err != ErrNotHandled { 18 | return nil, err 19 | } 20 | } 21 | //default load from disk 22 | b, err := ioutil.ReadFile(path) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return b, nil 27 | } 28 | 29 | func (w *AcmeWrapper) saveFile(path string, contents []byte) error { 30 | //use custom save file callback? 31 | if w.Config.SaveFileCallback != nil { 32 | if err := w.Config.SaveFileCallback(path, contents); err == nil { 33 | return nil 34 | } else if err != ErrNotHandled { 35 | return err 36 | } 37 | } 38 | //default save to disk (current user read+write only!) 39 | if err := ioutil.WriteFile(path, contents, 0600); err != nil { 40 | return err 41 | } 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /challengeprovider.go: -------------------------------------------------------------------------------- 1 | package acmewrapper 2 | 3 | import "github.com/xenolf/lego/acme" 4 | 5 | // wrapperChallengeProvider is used to fit into the acme.ChallengeProvider interface, 6 | // which allows us to modify our server during runtime to solve the SNI challenge 7 | type wrapperChallengeProvider struct { 8 | w *AcmeWrapper 9 | challengeDomain string 10 | } 11 | 12 | // Present sets up the challenge domain thru SNI. Part of acme.ChallengeProvider interface 13 | func (c *wrapperChallengeProvider) Present(domain, token, keyAuth string) error { 14 | logf("[acmewrapper] Started SNI server modification for %s", domain) 15 | 16 | // Use ACME's SNI challenge cert maker. How nice that it is exported :) 17 | cert, challengedomain, err := acme.TLSSNI01ChallengeCert(keyAuth) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | // Add the cert to our AcmeWrapper. here, the names is the special SNI challenge domain 23 | // in the form "..acme.invalid" 24 | c.w.AddSNI(challengedomain, &cert) 25 | 26 | c.challengeDomain = challengedomain 27 | 28 | return nil 29 | 30 | } 31 | 32 | // CleanUp removes the challenge domain from SNI. Part of acme.ChallengeProvider interface 33 | func (c *wrapperChallengeProvider) CleanUp(domain, token, keyAuth string) error { 34 | logf("[acmewrapper] End of SNI server modification for %s\n", domain) 35 | c.w.RemSNI(c.challengeDomain) 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /rwkey.go: -------------------------------------------------------------------------------- 1 | package acmewrapper 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/ecdsa" 7 | "crypto/rsa" 8 | "crypto/tls" 9 | "crypto/x509" 10 | "encoding/pem" 11 | "errors" 12 | ) 13 | 14 | // savePrivateKey is used to write the given key to file 15 | // This code copied from caddy: 16 | // https://github.com/mholt/caddy/blob/master/caddy/https/crypto.go 17 | func (w *AcmeWrapper) savePrivateKey(filename string, key crypto.PrivateKey) error { 18 | var pemType string 19 | var keyBytes []byte 20 | switch key := key.(type) { 21 | case *ecdsa.PrivateKey: 22 | var err error 23 | pemType = "EC" 24 | keyBytes, err = x509.MarshalECPrivateKey(key) 25 | if err != nil { 26 | return err 27 | } 28 | case *rsa.PrivateKey: 29 | pemType = "RSA" 30 | keyBytes = x509.MarshalPKCS1PrivateKey(key) 31 | } 32 | pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes} 33 | pemEncoded := bytes.Buffer{} 34 | if err := pem.Encode(&pemEncoded, &pemKey); err != nil { 35 | return err 36 | } 37 | return w.saveFile(filename, pemEncoded.Bytes()) 38 | } 39 | 40 | // loadPrivateKey reads a key from file 41 | // This code copied from caddy: 42 | // https://github.com/mholt/caddy/blob/master/caddy/https/crypto.go 43 | func (w *AcmeWrapper) loadPrivateKey(filename string) (crypto.PrivateKey, error) { 44 | keyBytes, err := w.loadFile(filename) 45 | if err != nil { 46 | return nil, err 47 | } 48 | keyBlock, _ := pem.Decode(keyBytes) 49 | switch keyBlock.Type { 50 | case "RSA PRIVATE KEY": 51 | return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) 52 | case "EC PRIVATE KEY": 53 | return x509.ParseECPrivateKey(keyBlock.Bytes) 54 | } 55 | return nil, errors.New("unknown private key type") 56 | } 57 | 58 | // AcmeWrapper version of LoadX509KeyPair reads and parses a public/private key pair from a pair of 59 | // files. The files must contain PEM encoded data. Pulled from std lib tls.go. 60 | func (w *AcmeWrapper) loadX509KeyPair(certFile, keyFile string) (tls.Certificate, error) { 61 | certPEMBlock, err := w.loadFile(certFile) 62 | if err != nil { 63 | return tls.Certificate{}, err 64 | } 65 | keyPEMBlock, err := w.loadFile(keyFile) 66 | if err != nil { 67 | return tls.Certificate{}, err 68 | } 69 | return tls.X509KeyPair(certPEMBlock, keyPEMBlock) 70 | } 71 | -------------------------------------------------------------------------------- /acme_test.go: -------------------------------------------------------------------------------- 1 | package acmewrapper 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestUserErrors(t *testing.T) { 12 | _, err := New(Config{ 13 | Server: TESTAPI, 14 | TOSCallback: TOSAgree, 15 | Address: TLSADDRESS, 16 | }) 17 | require.Error(t, err) 18 | _, err = New(Config{ 19 | Server: TESTAPI, 20 | Domains: TESTDOMAINS, 21 | Address: TLSADDRESS, 22 | }) 23 | require.Error(t, err) 24 | 25 | _, err = New(Config{ 26 | Server: TESTAPI, 27 | TOSCallback: TOSAgree, 28 | Domains: TESTDOMAINS, 29 | Address: TLSADDRESS, 30 | PrivateKeyFile: "testinguser.key", 31 | }) 32 | require.Error(t, err) 33 | 34 | _, err = New(Config{ 35 | Server: TESTAPI, 36 | TOSCallback: TOSAgree, 37 | Domains: TESTDOMAINS, 38 | Address: TLSADDRESS, 39 | RegistrationFile: "testinguser.reg", 40 | }) 41 | require.Error(t, err) 42 | 43 | _, err = New(Config{ 44 | Server: TESTAPI, 45 | TOSCallback: func(tosurl string) bool { 46 | fmt.Printf("TOS URL: %s\n", tosurl) 47 | return false 48 | }, 49 | Domains: TESTDOMAINS, 50 | }) 51 | 52 | require.Error(t, err) 53 | 54 | } 55 | 56 | func TestUser(t *testing.T) { 57 | // Test that an anonymous user can be successfully created 58 | 59 | w, err := New(Config{ 60 | Server: TESTAPI, 61 | TOSCallback: TOSAgree, 62 | Domains: TESTDOMAINS, 63 | Address: TLSADDRESS, 64 | }) 65 | 66 | require.NoError(t, err) 67 | require.Equal(t, w.GetEmail(), "") 68 | require.NotNil(t, w.GetRegistration()) 69 | require.NotNil(t, w.GetPrivateKey()) 70 | require.NotNil(t, w.GetCertificate()) 71 | require.False(t, w.CertNeedsUpdate()) 72 | 73 | os.Remove("testinguser.key") 74 | os.Remove("testinguser.reg") 75 | 76 | w, err = New(Config{ 77 | Server: TESTAPI, 78 | TOSCallback: TOSAgree, 79 | Domains: TESTDOMAINS, 80 | PrivateKeyFile: "testinguser.key", 81 | RegistrationFile: "testinguser.reg", 82 | Address: TLSADDRESS, 83 | }) 84 | 85 | require.NoError(t, err) 86 | 87 | // Now that the files are created, it should load fine without TOS 88 | w, err = New(Config{ 89 | Server: TESTAPI, 90 | TOSCallback: TOSDecline, 91 | Domains: TESTDOMAINS, 92 | PrivateKeyFile: "testinguser.key", 93 | RegistrationFile: "testinguser.reg", 94 | Address: TLSADDRESS, 95 | }) 96 | 97 | require.NoError(t, err) 98 | require.Equal(t, w.GetEmail(), "") 99 | require.NotNil(t, w.GetRegistration()) 100 | require.NotNil(t, w.GetPrivateKey()) 101 | } 102 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "flag" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/dkumor/acmewrapper" 11 | ) 12 | 13 | /* 14 | This example is the equivalent of SimpleHTTPServer in python, with the twist that it does 15 | Let's Encrypt certificates. 16 | */ 17 | 18 | var ( 19 | address = flag.String("host", ":443", "The address at which to run server") 20 | cert = flag.String("cert", "cert.crt", "Location of TLS certificate") 21 | key = flag.String("key", "key.pem", "Location of TLS key") 22 | reg = flag.String("reg", "user.reg", "Location to write user registration") 23 | priv = flag.String("priv", "userkey.pem", "Location to store user's private key") 24 | test = flag.Bool("test", false, "Use the Let's Encrypt staging server") 25 | acme = flag.String("server", acmewrapper.DefaultServer, "The ACME server to use") 26 | accept = flag.Bool("accept", false, "Accept the ACME server's TOS?") 27 | email = flag.String("email", "", "The email to use when registering") 28 | help = flag.Bool("help", false, "Show help message") 29 | ) 30 | 31 | func main() { 32 | flag.Parse() 33 | if *help || flag.NArg() < 2 { 34 | fmt.Printf("Usage: example -agree mywebsite.com www.mywebsite.com ./www\n will serve the ./www directory with TLS certs for mywebsite.com and www.mywebsite.com\n\n") 35 | flag.Usage() 36 | } 37 | if !*accept { 38 | fmt.Printf("To run the server, you must accept the Let's Encrypt TOS with -accept") 39 | os.Exit(1) 40 | } 41 | if *test { 42 | *acme = "https://acme-staging.api.letsencrypt.org/directory" 43 | } 44 | 45 | mux := http.NewServeMux() 46 | mux.Handle("/", http.FileServer(http.Dir(flag.Arg(flag.NArg()-1)))) 47 | 48 | w, err := acmewrapper.New(acmewrapper.Config{ 49 | Address: *address, 50 | 51 | Domains: flag.Args()[:flag.NArg()-1], 52 | 53 | Email: *email, 54 | 55 | TLSCertFile: *cert, 56 | TLSKeyFile: *key, 57 | 58 | RegistrationFile: *reg, 59 | PrivateKeyFile: *priv, 60 | 61 | Server: *acme, 62 | 63 | TOSCallback: acmewrapper.TOSAgree, 64 | }) 65 | if err != nil { 66 | fmt.Printf("ERROR: %s", err.Error()) 67 | os.Exit(1) 68 | } 69 | 70 | tlsconfig := w.TLSConfig() 71 | 72 | listener, err := tls.Listen("tcp", *address, tlsconfig) 73 | if err != nil { 74 | fmt.Printf("ERROR: %s", err.Error()) 75 | os.Exit(1) 76 | } 77 | 78 | fmt.Printf("\n\nRunning server at %s\n\n", *address) 79 | 80 | // In order to enable http2, we can't just use http.Serve in go1.6, so we need 81 | // to create a manual http.Server, since it needs the tlsconfig 82 | // https://github.com/golang/go/issues/14374 83 | server := &http.Server{ 84 | Addr: *address, 85 | Handler: mux, 86 | TLSConfig: tlsconfig, 87 | } 88 | server.Serve(listener) 89 | } 90 | -------------------------------------------------------------------------------- /cert.go: -------------------------------------------------------------------------------- 1 | package acmewrapper 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "io/ioutil" 7 | "os" 8 | "time" 9 | 10 | "github.com/xenolf/lego/acme" 11 | ) 12 | 13 | // writeCert takes an acme CertificateResource (as returned from the acme.RenewCertificate 14 | // and the acme.ObtainCertificate functions), and writes the cert and key files from it. 15 | // If the files already exist, it renames the old versions by adding .bak to them. This makes 16 | // sure that a little accident doesn't cause too much damage. 17 | func (w *AcmeWrapper) writeCert(certfile, keyfile string, crt acme.CertificateResource) (err error) { 18 | //If user has provided custom file handling, skip backups 19 | if w.Config.SaveFileCallback != nil { 20 | if err := w.saveFile(certfile, crt.Certificate); err != nil { 21 | return err 22 | } 23 | if err := w.saveFile(keyfile, crt.PrivateKey); err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | //If the files already exist, move them to backup 29 | err = os.Rename(certfile, certfile+".bak") 30 | if err != nil && !os.IsNotExist(err) { 31 | return err 32 | } 33 | err = os.Rename(keyfile, keyfile+".bak") 34 | if err != nil && !os.IsNotExist(err) { 35 | return err 36 | } 37 | err = ioutil.WriteFile(certfile, crt.Certificate, 0600) 38 | if err != nil { 39 | os.Rename(certfile+".bak", certfile) 40 | os.Rename(keyfile+".bak", keyfile) 41 | return err 42 | } 43 | err = ioutil.WriteFile(keyfile, crt.PrivateKey, 0600) 44 | if err != nil { 45 | os.Remove(certfile) 46 | os.Rename(certfile+".bak", certfile) 47 | os.Rename(keyfile+".bak", keyfile) 48 | return err 49 | } 50 | return nil 51 | } 52 | 53 | func tlsCert(crt acme.CertificateResource) (*tls.Certificate, error) { 54 | cert, err := tls.X509KeyPair(crt.Certificate, crt.PrivateKey) 55 | return &cert, err 56 | } 57 | 58 | // checks if a is a subset of b 59 | func arraySubset(a []string, b []string) bool { 60 | if len(a) > len(b) { 61 | return false 62 | } 63 | for _, i := range a { 64 | if !stringInSlice(i, b) { 65 | return false 66 | } 67 | } 68 | return true 69 | } 70 | 71 | // CertNeedsUpdate returns whether the current certificate either 72 | // does not exist, or is = 2) 125 | 126 | // Stop it from being annoying in the background anymore 127 | w.Config.RenewCheck = 9999 * 24 * time.Hour 128 | w.Config.RetryDelay = 9999 * 24 * time.Hour 129 | w.Config.RenewTime = 30 * time.Hour 130 | listener.Close() 131 | } 132 | 133 | // We now test to make sure that if the domain is changed, 134 | // the cert is changed 135 | func TestDomainChange(t *testing.T) { 136 | os.Remove("cert.crt") 137 | os.Remove("key.pem") 138 | newdomains := []string{"www." + TESTDOMAINS[0], TESTDOMAINS[0]} 139 | 140 | domainchanged := false 141 | 142 | // First set up the domain cert 143 | _, err := New(Config{ 144 | Server: TESTAPI, 145 | TOSCallback: TOSAgree, 146 | Domains: TESTDOMAINS, 147 | PrivateKeyFile: "testinguser.key", 148 | RegistrationFile: "testinguser.reg", 149 | Address: TLSADDRESS, 150 | 151 | TLSCertFile: "cert.crt", 152 | TLSKeyFile: "key.pem", 153 | 154 | RenewCallback: func() { 155 | domainchanged = true 156 | }, 157 | }) 158 | 159 | require.NoError(t, err) 160 | require.True(t, domainchanged) 161 | 162 | // Now running again will not renew 163 | domainchanged = false 164 | // First set up the domain cert 165 | _, err = New(Config{ 166 | Server: TESTAPI, 167 | TOSCallback: TOSAgree, 168 | Domains: TESTDOMAINS, 169 | PrivateKeyFile: "testinguser.key", 170 | RegistrationFile: "testinguser.reg", 171 | Address: TLSADDRESS, 172 | 173 | TLSCertFile: "cert.crt", 174 | TLSKeyFile: "key.pem", 175 | 176 | RenewCallback: func() { 177 | domainchanged = true 178 | }, 179 | }) 180 | require.NoError(t, err) 181 | require.False(t, domainchanged) 182 | 183 | // Finally, we run one more time, but this time we change the domains - and check to see if renew is called 184 | // Now running again will not call domainchanged 185 | domainchanged = false 186 | _, err = New(Config{ 187 | Server: TESTAPI, 188 | TOSCallback: TOSAgree, 189 | Domains: newdomains, 190 | PrivateKeyFile: "testinguser.key", 191 | RegistrationFile: "testinguser.reg", 192 | Address: TLSADDRESS, 193 | 194 | TLSCertFile: "cert.crt", 195 | TLSKeyFile: "key.pem", 196 | 197 | RenewCallback: func() { 198 | domainchanged = true 199 | }, 200 | }) 201 | require.NoError(t, err) 202 | require.True(t, domainchanged) 203 | } 204 | -------------------------------------------------------------------------------- /acme.go: -------------------------------------------------------------------------------- 1 | package acmewrapper 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/rsa" 9 | "encoding/json" 10 | "errors" 11 | "os" 12 | 13 | "github.com/xenolf/lego/acme" 14 | ) 15 | 16 | // generateKey generates a key to use for registration in acme 17 | func generateKey(keytype acme.KeyType) (crypto.PrivateKey, error) { 18 | switch keytype { 19 | case acme.RSA2048: 20 | return rsa.GenerateKey(rand.Reader, 2048) 21 | case acme.RSA4096: 22 | return rsa.GenerateKey(rand.Reader, 4096) 23 | case acme.RSA8192: 24 | return rsa.GenerateKey(rand.Reader, 8192) 25 | case acme.EC256: 26 | return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 27 | case acme.EC384: 28 | return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) 29 | default: 30 | return nil, errors.New("Unrecognized key type") 31 | } 32 | } 33 | 34 | // initACME initailizes the acme client - it does everything from reading/writing the 35 | // user private key and registration files, to ensuring that the user is registered 36 | // on the ACME server and has accepted the TOS. 37 | // It expects w.Config to be set up. 38 | // It sets up: 39 | // - w.privatekey 40 | // - w.registration 41 | // - w.client (init the user + agree to TOS) 42 | // - gets certificates if they don't exist or are close to expiration. 43 | // 44 | // Its input is whether there is a server running already. If the server is running, 45 | // then the SNI query will succeed. If it isn't (ie, we are just setting up), then 46 | // initACME must set up its own temporary server to get any initial certificates. 47 | func (w *AcmeWrapper) initACME(serverRunning bool) (err error) { 48 | // We are modifying and using some of the config properties, so lock them 49 | w.Lock() 50 | defer w.Unlock() 51 | 52 | // Just in case initACME is being run on an existing AcmeWrapper 53 | w.registration = nil 54 | w.privatekey = nil 55 | 56 | if len(w.Config.Domains) == 0 { 57 | return errors.New("No domains set - can't initialize ACME client") 58 | } 59 | if w.Config.TOSCallback == nil { 60 | return errors.New("TOSCallback is required: you need to agree to the terms of service") 61 | } 62 | 63 | if w.Config.PrivateKeyFile != "" { 64 | if w.Config.RegistrationFile == "" { 65 | return errors.New("A filename was set for the private key but not the registration file") 66 | } 67 | 68 | // We are to use file-backed registration. See if the files exist already. We first load 69 | // the key file, then we load the registration file 70 | w.privatekey, err = w.loadPrivateKey(w.Config.PrivateKeyFile) 71 | if err != nil { 72 | if !os.IsNotExist(err) { 73 | return err 74 | } 75 | // The private key file doesn't exist - w.privatekey is left at nil 76 | } 77 | 78 | regBytes, err := w.loadFile(w.Config.RegistrationFile) 79 | if err == nil { 80 | if err = json.Unmarshal(regBytes, &w.registration); err != nil { 81 | return err 82 | } 83 | } else if !os.IsNotExist(err) { 84 | return err 85 | } 86 | 87 | // If only one exists, but not the other, return an error. Reemember that these are nil if the file didn't exist 88 | if (w.privatekey != nil || w.registration != nil) && (w.privatekey == nil || w.registration == nil) { 89 | return errors.New("One of the files (registration or key) exists, but the other is missing") 90 | } 91 | 92 | } else if w.Config.RegistrationFile != "" { 93 | return errors.New("A filename was set for the registration file but not the private key") 94 | } 95 | 96 | if w.privatekey == nil { 97 | // If privatekey is nil, it means that either there are no files, or we are running in memory only 98 | // Whatever the case, we generate our acme user 99 | 100 | // Generate the key 101 | w.privatekey, err = generateKey(w.Config.PrivateKeyType) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | if w.Config.PrivateKeyFile != "" { 107 | // If we are to use a file, write it now 108 | if err = w.savePrivateKey(w.Config.PrivateKeyFile, w.privatekey); err != nil { 109 | return err 110 | } 111 | } 112 | 113 | } 114 | 115 | // Now that we have the key and necessary setup info, we prepare the ACME client. 116 | w.client, err = acme.NewClient(w.Config.Server, w, w.Config.PrivateKeyType) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | if w.registration == nil { 122 | // There is no registration - register with the ACME server 123 | w.registration, err = w.client.Register() 124 | if err != nil { 125 | return err 126 | } 127 | 128 | if !w.Config.TOSCallback(w.registration.TosURL) { 129 | return errors.New("Terms of service were not accepted") 130 | } 131 | 132 | if err = w.client.AgreeToTOS(); err != nil { 133 | return err 134 | } 135 | 136 | // If we are to use a registration file, write the file now 137 | if w.Config.RegistrationFile != "" { 138 | jsonBytes, err := json.MarshalIndent(w.registration, "", "\t") 139 | if err != nil { 140 | return err 141 | } 142 | if err = w.saveFile(w.Config.RegistrationFile, jsonBytes); err != nil { 143 | return err 144 | } 145 | } 146 | } 147 | 148 | // All of the challenges are disabled EXCEPT SNI 149 | w.client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01}) 150 | w.client.SetTLSAddress(w.Config.Address) 151 | 152 | // Now if we are to renew our certificate, do it now! We do this now if server is not running 153 | // yet, since in this case we use the default SNI provider, which runs a custom server. 154 | // We start a quick custom server 155 | // to get the initial certificates. In the future, we will use our custom SNI provider 156 | // to not need a custom server (and not have any downtime) while updating 157 | if !serverRunning && w.CertNeedsUpdate() { 158 | // Renew sets the config mutex, so unset it now 159 | w.Unlock() 160 | err = w.Renew() 161 | w.Lock() 162 | if err != nil { 163 | return err 164 | } 165 | } 166 | 167 | // Now that the user and client basics are initialized, we set up the client 168 | // so that it uses our custom SNI provider. We don't want 169 | // to start custom servers, but rather plug into our certificate updater once 170 | // we are running. This allows cert updates to be transparent. 171 | w.client.SetChallengeProvider(acme.TLSSNI01, &wrapperChallengeProvider{ 172 | w: w, 173 | }) 174 | 175 | // If our server IS running already, then we get our certificates NOW. The difference 176 | // between the above version and this one is that we use the custom provider if 177 | // the server is already active rather than starting a new server. 178 | 179 | if serverRunning && w.CertNeedsUpdate() { 180 | w.Unlock() 181 | err = w.Renew() 182 | w.Lock() 183 | if err != nil { 184 | return err 185 | } 186 | } 187 | 188 | return nil 189 | 190 | } 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **NOTE**: I recommend that new projects use the "official" acme library, [autocert](https://godoc.org/golang.org/x/crypto/acme/autocert). An example of usage can be seen [here](https://play.golang.org/p/Nas2lT_XeY). 2 | 3 | # ACMEWrapper 4 | 5 | Add Let's Encrypt support to your golang server in 10 lines of code. 6 | 7 | [![GoDoc](https://godoc.org/github.com/dkumor/acmewrapper?status.svg)](https://godoc.org/github.com/dkumor/acmewrapper) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/dkumor/acmewrapper)](https://goreportcard.com/report/github.com/dkumor/acmewrapper) 9 | [![Build Status](https://travis-ci.org/dkumor/acmewrapper.svg?branch=master)](https://travis-ci.org/dkumor/acmewrapper) 10 | 11 | ```go 12 | w, err := acmewrapper.New(acmewrapper.Config{ 13 | Domains: []string{"example.com","www.example.com"}, 14 | Address: ":443", 15 | 16 | TLSCertFile: "cert.pem", 17 | TLSKeyFile: "key.pem", 18 | 19 | // Let's Encrypt stuff 20 | RegistrationFile: "user.reg", 21 | PrivateKeyFile: "user.pem", 22 | 23 | TOSCallback: acmewrapper.TOSAgree, 24 | }) 25 | 26 | 27 | if err!=nil { 28 | log.Fatal("acmewrapper: ", err) 29 | } 30 | 31 | listener, err := tls.Listen("tcp", ":443", w.TLSConfig()) 32 | ``` 33 | 34 | Acmewrapper is built upon https://github.com/xenolf/lego, and handles all certificate generation, renewal 35 | 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. 36 | 37 | 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). 38 | 39 | 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! 40 | 41 | ## How It Works 42 | 43 | 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. 44 | 45 | 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. 46 | 47 | ## Notes 48 | 49 | Currently, Go 1.4 and above are supported 50 | 51 | - There was a breaking change on 3/20/16: all time periods in the configuration were switched from int64 to time.Duration. 52 | 53 | ## Example 54 | 55 | You can go into `./example` to find a sample basic http server that will serve a given folder over https with Let's Encrypt. 56 | 57 | Another simple example is given below: 58 | 59 | ### Old Code 60 | 61 | This is sample code before adding Let's Encrypt support: 62 | 63 | ```go 64 | package main 65 | 66 | import ( 67 | "io" 68 | "net/http" 69 | "log" 70 | ) 71 | 72 | func HelloServer(w http.ResponseWriter, req *http.Request) { 73 | io.WriteString(w, "hello, world!\n") 74 | } 75 | 76 | func main() { 77 | http.HandleFunc("/hello", HelloServer) 78 | err := http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil) 79 | if err != nil { 80 | log.Fatal("ListenAndServe: ", err) 81 | } 82 | } 83 | ``` 84 | 85 | ### New Code 86 | 87 | Adding let's encrypt support is a matter of setting the tls config: 88 | 89 | ```go 90 | package main 91 | 92 | import ( 93 | "io" 94 | "net/http" 95 | "log" 96 | "crypto/tls" 97 | 98 | "github.com/dkumor/acmewrapper" 99 | ) 100 | 101 | func HelloServer(w http.ResponseWriter, req *http.Request) { 102 | io.WriteString(w, "hello, world!\n") 103 | } 104 | 105 | func main() { 106 | mux := http.NewServeMux() 107 | mux.HandleFunc("/hello", HelloServer) 108 | 109 | w, err := acmewrapper.New(acmewrapper.Config{ 110 | Domains: []string{"example.com","www.example.com"}, 111 | Address: ":443", 112 | 113 | TLSCertFile: "cert.pem", 114 | TLSKeyFile: "key.pem", 115 | 116 | RegistrationFile: "user.reg", 117 | PrivateKeyFile: "user.pem", 118 | 119 | TOSCallback: acmewrapper.TOSAgree, 120 | }) 121 | 122 | 123 | if err!=nil { 124 | log.Fatal("acmewrapper: ", err) 125 | } 126 | 127 | tlsconfig := w.TLSConfig() 128 | 129 | listener, err := tls.Listen("tcp", ":443", tlsconfig) 130 | if err != nil { 131 | log.Fatal("Listener: ", err) 132 | } 133 | 134 | // To enable http2, we need http.Server to have reference to tlsconfig 135 | // https://github.com/golang/go/issues/14374 136 | server := &http.Server{ 137 | Addr: ":443", 138 | Handler: mux, 139 | TLSConfig: tlsconfig, 140 | } 141 | server.Serve(listener) 142 | } 143 | ``` 144 | 145 | ## Custom File Handlers 146 | 147 | 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 148 | 149 | ```go 150 | w, err := acmewrapper.New(acmewrapper.Config{ 151 | Domains: []string{"example.com","www.example.com"}, 152 | Address: ":443", 153 | 154 | TLSCertFile: "CERTIFICATE", 155 | TLSKeyFile: "TLSKEY", 156 | 157 | RegistrationFile: "REGISTRATION", 158 | PrivateKeyFile: "PRIVATEKEY", 159 | 160 | TOSCallback: acmewrapper.TOSAgree, 161 | 162 | SaveFileCallback: func(path string, contents []byte) error { 163 | // the path is the file name as set up in the configuration - the certificate will be "CERTIFICATE", etc. 164 | }, 165 | // If this callback does not find the file at the provided path, it must return os.ErrNotExist. 166 | // If this callback returns acmewrapper.ErrNotHandled, it will fallback to load file from disk. 167 | LoadFileCallback func(path string) (contents []byte, err error) { 168 | return os.ErrNotExist 169 | }, 170 | }) 171 | 172 | ``` 173 | 174 | ## Testing 175 | 176 | Running the tests is a bit of a chore, since it requires a valid domain name, and access to port 443. 177 | This is because ACMEWrapper uses the Let's Encrypt staging server to make sure the code is working. 178 | 179 | To test on your own server, you need to change the domain name to your domain, and set a custom testing port 180 | that will be routed to 443: 181 | 182 | ```bash 183 | go test -c 184 | sudo setcap cap_net_bind_service=+ep acmewrapper.test 185 | export TLSADDRESS=":443" 186 | export DOMAIN_NAME="example.com" 187 | ./acmewrapper.test 188 | ``` 189 | -------------------------------------------------------------------------------- /acmewrapper.go: -------------------------------------------------------------------------------- 1 | package acmewrapper 2 | 3 | import ( 4 | "crypto" 5 | "crypto/tls" 6 | "log" 7 | "os" 8 | "sync" 9 | 10 | "github.com/xenolf/lego/acme" 11 | ) 12 | 13 | // LoggerInterface represents anything that can Printf. 14 | type LoggerInterface interface { 15 | Printf(format string, v ...interface{}) 16 | } 17 | 18 | // Logger allows to use a custom logger for logging purposes 19 | var Logger LoggerInterface 20 | 21 | func logf(s string, v ...interface{}) { 22 | if Logger == nil { 23 | log.Printf(s, v...) 24 | } else { 25 | Logger.Printf(s, v...) 26 | } 27 | } 28 | 29 | // AcmeWrapper is the main object which controls tls certificates and their renewals 30 | type AcmeWrapper struct { 31 | sync.Mutex // configmutex ensures that settings for the ACME stuff don't happen in parallel 32 | Config Config 33 | 34 | certmutex sync.RWMutex // certmutex is used to make sure that replacing certificates doesn't asplode 35 | 36 | // Our user's private key & registration. Both are needed in order to be able 37 | // to renew/generate new certs. 38 | privatekey crypto.PrivateKey 39 | registration *acme.RegistrationResource 40 | 41 | // A map of custom certificates associated with special SNIs. The SNI request 42 | // passes through here 43 | certs map[string]*tls.Certificate 44 | 45 | // The current TLS cert used for SSL requests when the SNI doesn't match the map 46 | cert *tls.Certificate 47 | 48 | // The ACME client 49 | client *acme.Client 50 | } 51 | 52 | // GetEmail returns the user email (if any) 53 | // NOTE: NOT threadsafe 54 | func (w *AcmeWrapper) GetEmail() string { 55 | return w.Config.Email 56 | } 57 | 58 | // GetRegistration returns the registration currently being used 59 | // NOTE: NOT threadsafe 60 | func (w *AcmeWrapper) GetRegistration() *acme.RegistrationResource { 61 | return w.registration 62 | } 63 | 64 | // GetPrivateKey returns the private key for the given user. 65 | // NOTE: NOT threadsafe 66 | func (w *AcmeWrapper) GetPrivateKey() crypto.PrivateKey { 67 | return w.privatekey 68 | } 69 | 70 | // GetCertificate returns the current TLS certificate 71 | func (w *AcmeWrapper) GetCertificate() *tls.Certificate { 72 | w.certmutex.RLock() 73 | defer w.certmutex.RUnlock() 74 | return w.cert 75 | } 76 | 77 | // AddSNI adds a domain name and certificate pair to the AcmeWrapper. 78 | // Whenever a request is for the passed domain, its associated certifcate is returned. 79 | func (w *AcmeWrapper) AddSNI(domain string, cert *tls.Certificate) { 80 | w.certmutex.Lock() 81 | defer w.certmutex.Unlock() 82 | 83 | w.certs[domain] = cert 84 | } 85 | 86 | // RemSNI removes a domain name and certificate pair from the AcmeWrapper. It is assumed that 87 | // they were added using AddSNI. 88 | func (w *AcmeWrapper) RemSNI(domain string) { 89 | w.certmutex.Lock() 90 | defer w.certmutex.Unlock() 91 | 92 | delete(w.certs, domain) 93 | } 94 | 95 | // TLSConfigGetCertificate is the main function used in the ACME wrapper. This is set in tls.Config to 96 | // the GetCertificate property. Note that Certificates must be empty for it to be called 97 | // correctly, so unless you know what you're doing, just use AcmeWrapper.TLSConfig() 98 | func (w *AcmeWrapper) TLSConfigGetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { 99 | w.certmutex.RLock() 100 | defer w.certmutex.RUnlock() 101 | 102 | // If the SNI is in the certs map, return that cert 103 | if _, ok := w.certs[clientHello.ServerName]; ok { 104 | return w.certs[clientHello.ServerName], nil 105 | } 106 | 107 | // Otherwise, return the default cert 108 | return w.cert, nil 109 | } 110 | 111 | // TLSConfig returns a TLS configuration that will automatically work with the golang ssl listener. 112 | // This sets it up so that the server automatically uses a working cert, and updates the cert when 113 | // necessary. 114 | func (w *AcmeWrapper) TLSConfig() *tls.Config { 115 | return &tls.Config{ 116 | //Go 1.6 "allows Listen to succeed when the Config has a nil Certificates, as long as the 117 | //GetCertificate callback is set" See https://golang.org/doc/go1.6#minor_library_changes. 118 | //So to add Go 1.5 support, we provide a certificate that will never be used. 119 | // This must be a valid cert, since if the client does not have SNI enabled in go1.4, 120 | // then there could be issues. AcmeWrapper does not support clients without SNI enabled at this time. 121 | Certificates: []tls.Certificate{*w.cert}, 122 | GetCertificate: w.TLSConfigGetCertificate, 123 | } 124 | } 125 | 126 | // AcmeDisabled allows to enable/disable acme-based certificate. Note that it is assumed that 127 | // this function is only called during server runtime (ie, your server is already listening). 128 | // its main purpose is to enable live reload of acme configuration. Do NOT set AcmeDisabled 129 | // in AcmeWrapper.Config, since it will panic. 130 | func (w *AcmeWrapper) AcmeDisabled(set bool) error { 131 | w.Lock() 132 | w.Config.AcmeDisabled = set 133 | w.Unlock() 134 | if !set && w.client == nil { 135 | return w.initACME(true) 136 | } 137 | return nil 138 | } 139 | 140 | // SetNewCert loads a new TLS key/cert from the given files. Running it with the same 141 | // filenames as existing cert will reload them 142 | func (w *AcmeWrapper) SetNewCert(certfile, keyfile string) error { 143 | cert, err := w.loadX509KeyPair(certfile, keyfile) 144 | if err != nil { 145 | return err 146 | } 147 | w.certmutex.Lock() 148 | w.cert = &cert 149 | w.certmutex.Unlock() 150 | 151 | return nil 152 | } 153 | 154 | // New generates an AcmeWrapper given a configuration 155 | func New(c Config) (*AcmeWrapper, error) { 156 | var err error 157 | // First, set up the default values for any settings that require 158 | // values 159 | if c.Server == "" { 160 | c.Server = DefaultServer 161 | } 162 | if c.PrivateKeyType == "" { 163 | c.PrivateKeyType = DefaultKeyType 164 | } 165 | if c.RenewTime == 0 { 166 | c.RenewTime = DefaultRenewTime 167 | } 168 | if c.RetryDelay == 0 { 169 | c.RetryDelay = DefaultRetryDelay 170 | } 171 | if c.RenewCheck == 0 { 172 | c.RenewCheck = DefaultRenewCheck 173 | } 174 | if c.Address == "" { 175 | c.Address = DefaultAddress 176 | } 177 | 178 | // Now set up the actual wrapper 179 | 180 | var w AcmeWrapper 181 | w.Config = c 182 | w.certs = make(map[string]*tls.Certificate) 183 | 184 | // Now load up the key and cert files for TLS if they are set 185 | if c.TLSKeyFile != "" || c.TLSCertFile != "" { 186 | err = w.SetNewCert(c.TLSCertFile, c.TLSKeyFile) 187 | if err != nil { 188 | if !os.IsNotExist(err) || c.AcmeDisabled { 189 | // The TLS key and cert file are only 190 | // allowed to not be there if ACME will generate them 191 | // TODO: We don't check here if both are missing vs 1 missing 192 | return nil, err 193 | } 194 | 195 | } 196 | } 197 | 198 | // If acme is enabled, initialize it! 199 | if !c.AcmeDisabled { 200 | // Initialize the ACME user 201 | // initUser succeeding initializes: 202 | // - w.privatekey 203 | // - w.registration 204 | // - w.client 205 | if err = w.initACME(false); err != nil { 206 | return nil, err 207 | } 208 | } 209 | 210 | // Finally, start the background routine 211 | go backgroundExpirationChecker(&w) 212 | 213 | return &w, nil 214 | } 215 | --------------------------------------------------------------------------------